diff --git a/jadx-core/clsp-data/android-5.1.jar b/jadx-core/clsp-data/android-5.1.jar index 8e171f7675f..68b8a554121 100644 Binary files a/jadx-core/clsp-data/android-5.1.jar and b/jadx-core/clsp-data/android-5.1.jar differ diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 6a404b60158..b78aa9cdc42 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -3,7 +3,6 @@ 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.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 ExecutorService getSaveExecutor() { 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 void run() { } 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 @@ RootNode getRoot() { return root; } - BinaryXMLParser getXmlParser() { + synchronized BinaryXMLParser getXmlParser() { if (xmlParser == null) { xmlParser = new BinaryXMLParser(root); } @@ -321,4 +311,5 @@ public IJadxArgs getArgs() { public String toString() { return "jadx decompiler " + getVersion(); } + } diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java index cb661fc48ef..6e739ff17db 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFile.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -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 ResourceType getType() { return type; } - public CodeWriter getContent() { + public ResContainer getContent() { return ResourcesLoader.loadContent(decompiler, this); } diff --git a/jadx-core/src/main/java/jadx/api/ResourceFileContent.java b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java new file mode 100644 index 00000000000..8525ec46d33 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java @@ -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); + } +} diff --git a/jadx-core/src/main/java/jadx/api/ResourceType.java b/jadx-core/src/main/java/jadx/api/ResourceType.java index dae2805b602..3399f8ca60d 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceType.java +++ b/jadx-core/src/main/java/jadx/api/ResourceType.java @@ -34,7 +34,6 @@ public static ResourceType getFileType(String fileName) { public static boolean isSupportedForUnpack(ResourceType type) { switch (type) { case CODE: - case ARSC: case LIB: case FONT: case IMG: @@ -43,6 +42,7 @@ public static boolean isSupportedForUnpack(ResourceType type) { case MANIFEST: case XML: + case ARSC: return true; } return false; diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 8a9dae10171..eda08ec82ba 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -5,6 +5,7 @@ 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 @@ List load(List inputFiles) { } 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 static Object decodeStream(ResourceFile rf, ResourceDecoder decoder) thro 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 Object decode(long size, InputStream is) throws IOException { 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 list, File file) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index 8ca5a4e0c6d..160f085005f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -10,6 +10,7 @@ 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 void loadResources(List resources) { 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; } diff --git a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java index 2f707d07dd5..3385321eda4 100644 --- a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java @@ -27,30 +27,14 @@ public static String unescapeChar(char ch) { 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 static String escapeXML(String str) { } 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(); + } } 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 cce1ca17d25..b334591bf57 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -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 BinaryXMLParser(RootNode root) { resNames = root.getResourcesNames(); attributes = new ManifestAttributes(); - attributes.parse(); + attributes.parseAll(); } catch (Exception e) { throw new JadxRuntimeException("BinaryXMLParser init error", e); } @@ -221,12 +222,13 @@ private void parseElement() throws IOException { 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 @@ private void parseElement() throws IOException { 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 @@ private void parseAttribute(int i) throws IOException { } 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 @@ private void parseAttribute(int i) throws IOException { } else { decodeAttribute(attributeNS, attrValDataType, attrValData); } + writer.add('"'); } private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData) { @@ -295,7 +298,11 @@ private void decodeAttribute(int attributeNS, int attrValDataType, int attrValDa 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) { 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 9f4710419f0..35f51fbce2e 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ManifestAttributes.java @@ -4,6 +4,8 @@ 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.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 @@ private enum MAttrType { private static class MAttr { private final MAttrType type; - private final Map values = new LinkedHashMap(); + private final Map values = new LinkedHashMap(); public MAttr(MAttrType type) { this.type = type; @@ -37,7 +41,7 @@ public MAttrType getType() { return type; } - public Map getValues() { + public Map getValues() { return values; } @@ -47,15 +51,23 @@ public String toString() { } } - private final Document doc; private final Map attrMap = new HashMap(); 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 ManifestAttributes() throws Exception { 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 @@ private void parseValues(String name, NodeList nodeList) { 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 @@ private void parseValues(String name, NodeList nodeList) { } } - 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 String decode(String attrName, int value) { } } else if (attr.getType() == MAttrType.FLAG) { StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : attr.getValues().entrySet()) { + for (Map.Entry entry : attr.getValues().entrySet()) { if ((value & entry.getKey()) != 0) { sb.append(entry.getValue()).append('|'); } @@ -166,6 +179,6 @@ public String decode(String attrName, int value) { return sb.deleteCharAt(sb.length() - 1).toString(); } } - return "UNKNOWN_DATA_0x" + Integer.toHexString(value); + return "UNKNOWN_DATA_0x" + Long.toHexString(value); } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java b/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java index 33b2ab0743c..53bcbd5a150 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ParserConstants.java @@ -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 PLURALS_MAP = new HashMap() { + { + 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; } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java new file mode 100644 index 00000000000..d0bf76d23a1 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java @@ -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 { + + private final String name; + @Nullable + private CodeWriter content; + + private final List subFiles; + + private ResContainer(String name, @Nullable CodeWriter content, List subFiles) { + this.name = name; + this.content = content; + this.subFiles = subFiles; + } + + public static ResContainer singleFile(String name, CodeWriter content) { + return new ResContainer(name, content, Collections.emptyList()); + } + + public static ResContainer multiFile(String name) { + return new ResContainer(name, null, new ArrayList()); + } + + 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 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 + + "}"; + } +} 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 3e0c98a8da7..e769984cc98 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -58,9 +58,19 @@ public void decode(InputStream inputStream) throws IOException { 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(); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java new file mode 100644 index 00000000000..a093321fe68 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -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 SKIP_RES_TYPES = new HashSet(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 makeResourcesXml() { + Map contMap = new HashMap(); + 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(""); + cw.startLine(""); + cw.incIndent(); + contMap.put(fn, cw); + } + addValue(cw, ri); + } + + List files = new ArrayList(contMap.size()); + for (Map.Entry entry : contMap.entrySet()) { + String fileName = entry.getKey(); + CodeWriter content = entry.getValue(); + + content.decIndent(); + content.startLine(""); + 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("'); + } + } + + 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("'); + } + + 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(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java new file mode 100644 index 00000000000..97260ded69b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -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 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); + } + } + } +} 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 99731003ce7..91ab3550950 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 @@ -20,8 +20,7 @@ public String getCountry() { return country; } - @Override - public String toString() { + public String getLocale() { StringBuilder sb = new StringBuilder(); if (language != null) { sb.append(language); @@ -29,6 +28,13 @@ public String toString() { 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(']'); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java index c75da989ff6..2f947c48b37 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/RawNamedValue.java @@ -16,4 +16,9 @@ public int getNameRef() { public RawValue getRawValue() { return rawValue; } + + @Override + public String toString() { + return "RawNamedValue{nameRef=" + nameRef + ", rawValue=" + rawValue + '}'; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 395a4d49501..654f6ce7697 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -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 enum JResType { } private final String name; + private final String shortName; private final List files = new ArrayList(1); private final JResType type; private final ResourceFile resFile; @@ -40,12 +43,18 @@ public enum JResType { private Map 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 final void update() { } } + protected void loadContent() { + getContent(); + for (JResource res : files) { + res.loadContent(); + } + } + public String getName() { return name; } @@ -65,16 +81,64 @@ public String getContent() { 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 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 int compareTo(JResource o) { @Override public String makeString() { - return name; + return shortName; } @Override