Commit 3700ecb7 authored by Skylot's avatar Skylot

core: add resources methods to jadx API

parent 811b0e7f
......@@ -32,6 +32,12 @@ public final class JadxCLIArgs implements IJadxArgs {
@Parameter(names = {"-f", "--fallback"}, description = "make simple dump (using goto instead of 'if', 'for', etc)")
protected boolean fallbackMode = false;
@Parameter(names = {"-r", "--no-res"}, description = "do not decode resources")
protected boolean skipResources = false;
@Parameter(names = {"-s", "--no-src"}, description = "do not decompile source code")
protected boolean skipSources = false;
@Parameter(names = {"--show-bad-code"}, description = "show inconsistent code (incorrectly decompiled)")
protected boolean showInconsistentCode = false;
......@@ -47,9 +53,6 @@ public final class JadxCLIArgs implements IJadxArgs {
@Parameter(names = {"-h", "--help"}, description = "print this help", help = true)
protected boolean printHelp = false;
@Parameter(names = {"-x", "--xml"}, description = "try to decode the AndroidManifest.xml")
protected boolean xmlTest = false;
private final List<File> input = new ArrayList<File>(1);
private File outputDir;
......@@ -168,8 +171,13 @@ public final class JadxCLIArgs implements IJadxArgs {
}
@Override
public boolean isXMLTest() {
return xmlTest;
public boolean isSkipResources() {
return skipResources;
}
@Override
public boolean isSkipSources() {
return skipSources;
}
@Override
......
......@@ -40,7 +40,12 @@ public class DefaultJadxArgs implements IJadxArgs {
}
@Override
public boolean isXMLTest() {
public boolean isSkipResources() {
return false;
}
@Override
public boolean isSkipSources() {
return false;
}
}
......@@ -17,5 +17,7 @@ public interface IJadxArgs {
boolean isVerbose();
boolean isXMLTest();
boolean isSkipResources();
boolean isSkipSources();
}
......@@ -2,6 +2,7 @@ package jadx.api;
import jadx.core.Jadx;
import jadx.core.ProcessClass;
import jadx.core.codegen.CodeWriter;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
......@@ -55,6 +56,9 @@ public final class JadxDecompiler {
private RootNode root;
private List<IDexTreeVisitor> passes;
private List<JavaClass> classes;
private List<ResourceFile> resources;
private BinaryXMLParser xmlParser;
public JadxDecompiler() {
this(new DefaultJadxArgs());
......@@ -82,6 +86,8 @@ public final class JadxDecompiler {
void reset() {
ClassInfo.clearCache();
classes = null;
resources = null;
xmlParser = null;
root = null;
}
......@@ -108,27 +114,21 @@ public final class JadxDecompiler {
parse();
}
public void parseAndSaveXML() {
if (this.args.isXMLTest()) {
InputFile inf = inputFiles.get(0);
try {
byte[] buffer = InputFile.loadXMLBuffer(inf.getFile());
if (buffer != null) {
File out = new File(outDir, "AndroidManifest.xml");
BinaryXMLParser bxp = new BinaryXMLParser(root);
bxp.parse(buffer, out);
}
} catch (Exception e) {
LOG.info("Decompiling AndroidManifest.xml failed!", e);
}
public void save() {
save(!args.isSkipSources(), !args.isSkipResources());
}
public void saveSources() {
save(true, false);
}
public void save() {
parseAndSaveXML();
public void saveResources() {
save(false, true);
}
private void save(boolean saveSources, boolean saveResources) {
try {
ExecutorService ex = getSaveExecutor();
ExecutorService ex = getSaveExecutor(saveSources, saveResources);
ex.shutdown();
ex.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
......@@ -137,6 +137,10 @@ public final class JadxDecompiler {
}
public ExecutorService getSaveExecutor() {
return getSaveExecutor(!args.isSkipSources(), !args.isSkipResources());
}
private ExecutorService getSaveExecutor(boolean saveSources, boolean saveResources) {
if (root == null) {
throw new JadxRuntimeException("No loaded files");
}
......@@ -145,6 +149,7 @@ public final class JadxDecompiler {
LOG.info("processing ...");
ExecutorService executor = Executors.newFixedThreadPool(threadsCount);
if (saveSources) {
for (final JavaClass cls : getClasses()) {
executor.execute(new Runnable() {
@Override
......@@ -154,6 +159,22 @@ 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()));
}
}
}
});
}
}
return executor;
}
......@@ -172,6 +193,16 @@ public final class JadxDecompiler {
return classes;
}
public List<ResourceFile> getResources() {
if (resources == null) {
if (root == null) {
return Collections.emptyList();
}
resources = new ResourcesLoader(this).load(inputFiles);
}
return resources;
}
public List<JavaPackage> getPackages() {
List<JavaClass> classList = getClasses();
if (classList.isEmpty()) {
......@@ -232,6 +263,13 @@ public final class JadxDecompiler {
return root;
}
BinaryXMLParser getXmlParser() {
if (xmlParser == null) {
xmlParser = new BinaryXMLParser(root);
}
return xmlParser;
}
JavaClass findJavaClass(ClassNode cls) {
if (cls == null) {
return null;
......
package jadx.api;
import jadx.core.codegen.CodeWriter;
import java.io.File;
public class ResourceFile {
public static final class ZipRef {
private final File zipFile;
private final String entryName;
public ZipRef(File zipFile, String entryName) {
this.zipFile = zipFile;
this.entryName = entryName;
}
public File getZipFile() {
return zipFile;
}
public String getEntryName() {
return entryName;
}
@Override
public String toString() {
return "ZipRef{" + zipFile + ", '" + entryName + "'}";
}
}
private final JadxDecompiler decompiler;
private final String name;
private final ResourceType type;
private ZipRef zipRef;
ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
this.decompiler = decompiler;
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public ResourceType getType() {
return type;
}
public CodeWriter getContent() {
return ResourcesLoader.loadContent(decompiler, zipRef, type);
}
void setZipRef(ZipRef zipRef) {
this.zipRef = zipRef;
}
@Override
public String toString() {
return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}";
}
}
package jadx.api;
public enum ResourceType {
CODE(".dex", ".class"),
MANIFEST("AndroidManifest.xml"),
XML(".xml"), // TODO binary or not?
ARSC(".arsc"), // TODO decompile !!!
FONT(".ttf"),
IMG(".png", ".gif", ".jpg"),
LIB(".so"),
UNKNOWN;
private String[] exts;
ResourceType(String... exts) {
this.exts = exts;
}
public String[] getExts() {
return exts;
}
public static ResourceType getFileType(String fileName) {
for (ResourceType type : ResourceType.values()) {
for (String ext : type.getExts()) {
if (fileName.endsWith(ext)) {
return type;
}
}
}
return UNKNOWN;
}
public static boolean isSupportedForUnpack(ResourceType type) {
switch (type) {
case CODE:
case ARSC:
case LIB:
case XML:
case FONT:
case IMG:
case UNKNOWN:
return false;
case MANIFEST:
return true;
}
return false;
}
}
package jadx.api;
import jadx.api.ResourceFile.ZipRef;
import jadx.core.codegen.CodeWriter;
import jadx.core.utils.files.InputFile;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// TODO: move to core package
final class ResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private static final int READ_BUFFER_SIZE = 8 * 1024;
private static final int LOAD_SIZE_LIMIT = 500 * 1024;
private JadxDecompiler jadxRef;
ResourcesLoader(JadxDecompiler jadxRef) {
this.jadxRef = jadxRef;
}
List<ResourceFile> load(List<InputFile> inputFiles) {
List<ResourceFile> list = new ArrayList<ResourceFile>(inputFiles.size());
for (InputFile file : inputFiles) {
loadFile(list, file.getFile());
}
return list;
}
static CodeWriter loadContent(JadxDecompiler jadxRef, ZipRef zipRef, ResourceType type) {
if (zipRef == null) {
return null;
}
ZipFile zipFile = null;
InputStream inputStream = null;
try {
zipFile = new ZipFile(zipRef.getZipFile());
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
if (entry != null) {
if (entry.getSize() > LOAD_SIZE_LIMIT) {
return new CodeWriter().add("File too big, size: "
+ String.format("%.2f KB", entry.getSize() / 1024.));
}
inputStream = new BufferedInputStream(zipFile.getInputStream(entry));
return decode(jadxRef, type, inputStream);
} else {
LOG.warn("Zip entry not found: {}", zipRef);
}
} catch (IOException e) {
LOG.error("Error load: " + zipRef, e);
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
LOG.debug("Error close zip file: " + zipRef, e);
}
}
return null;
}
private static CodeWriter decode(JadxDecompiler jadxRef, ResourceType type,
InputStream inputStream) throws IOException {
switch (type) {
case MANIFEST:
case XML:
return jadxRef.getXmlParser().parse(inputStream);
}
return loadToCodeWriter(inputStream);
}
private void loadFile(List<ResourceFile> list, File file) {
if (file == null) {
return;
}
ZipFile zip = null;
try {
zip = new ZipFile(file);
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
addEntry(list, file, entry);
}
} catch (IOException e) {
LOG.debug("Not a zip file: " + file.getAbsolutePath());
} finally {
if (zip != null) {
try {
zip.close();
} catch (Exception e) {
LOG.error("Zip file close error: " + file.getAbsolutePath(), e);
}
}
}
}
private void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry) {
if (entry.isDirectory()) {
return;
}
String name = entry.getName();
ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = new ResourceFile(jadxRef, name, type);
rf.setZipRef(new ZipRef(zipFile, name));
list.add(rf);
// LOG.debug("Add resource entry: {}, size: {}", name, entry.getSize());
}
private static CodeWriter loadToCodeWriter(InputStream is) throws IOException {
CodeWriter cw = new CodeWriter();
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
byte[] buffer = new byte[READ_BUFFER_SIZE];
int count;
try {
while ((count = is.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
} finally {
try {
is.close();
} catch (Exception ignore) {
}
}
cw.add(baos.toString("UTF-8"));
return cw;
}
}
......@@ -5,11 +5,12 @@ import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.DexNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
......@@ -32,9 +33,11 @@ public class BinaryXMLParser {
private static final Logger LOG = LoggerFactory.getLogger(BinaryXMLParser.class);
private byte[] bytes;
private static final Charset STRING_CHARSET = Charset.forName("UTF-16LE");
private CodeWriter writer;
private InputStream input;
private String[] strings;
private int count;
private String nsPrefix = "ERROR";
private String nsURI = "ERROR";
......@@ -43,9 +46,9 @@ public class BinaryXMLParser {
private boolean firstElement;
private boolean wasOneLiner = false;
private CodeWriter writer;
private Map<Integer, String> styleMap = new HashMap<Integer, String>();
private Map<Integer, FieldNode> localStyleMap = new HashMap<Integer, FieldNode>();
private final ManifestAttributes attributes;
public BinaryXMLParser(RootNode root) {
......@@ -70,30 +73,34 @@ public class BinaryXMLParser {
}
}
public void parse(byte[] xmlBytes, File out) {
LOG.debug("Decoding AndroidManifest.xml, output: {}", out);
public synchronized CodeWriter parse(InputStream inputStream) {
writer = new CodeWriter();
writer.add("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
bytes = xmlBytes;
count = 0;
input = inputStream;
firstElement = true;
try {
decode();
writer.save(out);
} catch (IOException e) {
LOG.debug("Binary xml decode failed", e);
CodeWriter cw = new CodeWriter();
cw.add("Error decode binary xml");
cw.startLine(Utils.getStackTrace(e));
return cw;
}
writer.finish();
return writer;
}
void decode() {
if (cInt16(bytes, count) != 0x0003) {
void decode() throws IOException {
if (cInt16() != 0x0003) {
die("Version is not 3");
}
if (cInt16(bytes, count) != 0x0008) {
if (cInt16() != 0x0008) {
die("Size of header is not 8");
}
if (cInt32(bytes, count) != bytes.length) {
die("Size of manifest doesn't match");
}
while ((count + 2) <= bytes.length) {
int type = cInt16(bytes, count);
cInt32();
while (input.available() != 0) {
int type = cInt16();
switch (type) {
case 0x0001:
parseStringPool();
......@@ -124,105 +131,106 @@ public class BinaryXMLParser {
}
}
private void parseStringPool() {
if (cInt16(bytes, count) != 0x001c) {
private void parseStringPool() throws IOException {
if (cInt16() != 0x001c) {
die("Header header size not 28");
}
int hsize = cInt32(bytes, count);
int stringCount = cInt32(bytes, count);
int styleCount = cInt32(bytes, count);
int flags = cInt32(bytes, count);
int stringsStart = cInt32(bytes, count);
int stylesStart = cInt32(bytes, count);
int hsize = cInt32();
int stringCount = cInt32();
int styleCount = cInt32();
int flags = cInt32();
int stringsStart = cInt32();
int stylesStart = cInt32();
int[] stringsOffsets = new int[stringCount];
for (int i = 0; i < stringCount; i++) {
stringsOffsets[i] = cInt32(bytes, count);
stringsOffsets[i] = cInt32();
}
strings = new String[stringCount];
for (int i = 0; i < stringCount; i++) {
int off = 8 + stringsStart + stringsOffsets[i];
int strlen = cInt16(bytes, off);
int strlen = cInt16();
byte[] str = new byte[strlen * 2];
System.arraycopy(bytes, count, str, 0, strlen * 2);
count += strlen * 2;
strings[i] = new String(str, Charset.forName("UTF-16LE"));
count += 2;
readToArray(str);
strings[i] = new String(str, STRING_CHARSET);
cInt16();
}
}
private void parseResourceMap() {
if (cInt16(bytes, count) != 0x8) {
private void parseResourceMap() throws IOException {
if (cInt16() != 0x8) {
die("Header size of resmap is not 8!");
}
int rhsize = cInt32(bytes, count);
int rhsize = cInt32();
int[] ids = new int[(rhsize - 8) / 4];
for (int i = 0; i < ids.length; i++) {
ids[i] = cInt32(bytes, count);
ids[i] = cInt32();
}
}
private void parseNameSpace() {
if (cInt16(bytes, count) != 0x0010) {
private void parseNameSpace() throws IOException {
if (cInt16() != 0x10) {
die("NAMESPACE header is not 0x0010");
}
if (cInt32(bytes, count) != 0x18) {
if (cInt32() != 0x18) {
die("NAMESPACE header chunk is not 0x18 big");
}
int beginLineNumber = cInt32(bytes, count);
int comment = cInt32(bytes, count);
int beginPrefix = cInt32(bytes, count);
int beginLineNumber = cInt32();
int comment = cInt32();
int beginPrefix = cInt32();
nsPrefix = strings[beginPrefix];
int beginURI = cInt32(bytes, count);
int beginURI = cInt32();
nsURI = strings[beginURI];
}
private void parseNameSpaceEnd() {
if (cInt16(bytes, count) != 0x0010) {
private void parseNameSpaceEnd() throws IOException {
if (cInt16() != 0x10) {
die("NAMESPACE header is not 0x0010");
}
if (cInt32(bytes, count) != 0x18) {
if (cInt32() != 0x18) {
die("NAMESPACE header chunk is not 0x18 big");
}
int endLineNumber = cInt32(bytes, count);
int comment = cInt32(bytes, count);
int endPrefix = cInt32(bytes, count);
int endLineNumber = cInt32();
int comment = cInt32();
int endPrefix = cInt32();
nsPrefix = strings[endPrefix];
int endURI = cInt32(bytes, count);
int endURI = cInt32();
nsURI = strings[endURI];
}
private void parseElement() {
private void parseElement() throws IOException {
if (firstElement) {
firstElement = false;
} else {
writer.incIndent();
}
if (cInt16(bytes, count) != 0x0010) {
if (cInt16() != 0x10) {
die("ELEMENT HEADER SIZE is not 0x10");
}
count += 4; // TODO: Check element chunk size
int elementBegLineNumber = cInt32(bytes, count);
int comment = cInt32(bytes, count);
int startNS = cInt32(bytes, count);
int startNSName = cInt32(bytes, count); // actually is elementName...
// TODO: Check element chunk size
cInt32();
int elementBegLineNumber = cInt32();
int comment = cInt32();
int startNS = cInt32();
int startNSName = cInt32(); // actually is elementName...
if (!wasOneLiner && !"ERROR".equals(currentTag) && !currentTag.equals(strings[startNSName])) {
writer.add(">");
}
wasOneLiner = false;
currentTag = strings[startNSName];
writer.startLine("<").add(strings[startNSName]);
int attributeStart = cInt16(bytes, count);
writer.attachSourceLine(elementBegLineNumber);
int attributeStart = cInt16();
if (attributeStart != 0x14) {
die("startNS's attributeStart is not 0x14");
}
int attributeSize = cInt16(bytes, count);
int attributeSize = cInt16();
if (attributeSize != 0x14) {
die("startNS's attributeSize is not 0x14");
}
int attributeCount = cInt16(bytes, count);
int idIndex = cInt16(bytes, count);
int classIndex = cInt16(bytes, count);
int styleIndex = cInt16(bytes, count);
int attributeCount = cInt16();
int idIndex = cInt16();
int classIndex = cInt16();
int styleIndex = cInt16();
if ("manifest".equals(strings[startNSName])) {
writer.add(" xmlns:\"").add(nsURI).add("\"");
}
......@@ -230,18 +238,27 @@ public class BinaryXMLParser {
writer.add(" ");
}
for (int i = 0; i < attributeCount; i++) {
int attributeNS = cInt32(bytes, count);
int attributeName = cInt32(bytes, count);
int attributeRawValue = cInt32(bytes, count);
int attrValSize = cInt16(bytes, count);
parseAttribute(i);
writer.add('"');
if ((i + 1) < attributeCount) {
writer.add(" ");
}
}
}
private void parseAttribute(int i) throws IOException {
int attributeNS = cInt32();
int attributeName = cInt32();
int attributeRawValue = cInt32();
int attrValSize = cInt16();
if (attrValSize != 0x08) {
die("attrValSize != 0x08 not supported");
}
if (cInt8(bytes, count) != 0) {
if (cInt8() != 0) {
die("res0 is not 0");
}
int attrValDataType = cInt8(bytes, count);
int attrValData = cInt32(bytes, count);
int attrValDataType = cInt8();
int attrValData = cInt32();
if (attributeNS != -1) {
writer.add(nsPrefix).add(':');
}
......@@ -251,6 +268,11 @@ public class BinaryXMLParser {
if (decodedAttr != null) {
writer.add(decodedAttr);
} else {
decodeAttribute(attributeNS, attrValDataType, attrValData);
}
}
private void decodeAttribute(int attributeNS, int attrValDataType, int attrValData) {
switch (attrValDataType) {
case 0x3:
writer.add(strings[attrValData]);
......@@ -295,29 +317,24 @@ public class BinaryXMLParser {
break;
}
}
writer.add('"');
if ((i + 1) < attributeCount) {
writer.add(" ");
}
}
}
private void parseElementEnd() {
if (cInt16(bytes, count) != 0x0010) {
die("ELEMENT END header is not 0x0010");
private void parseElementEnd() throws IOException {
if (cInt16() != 0x10) {
die("ELEMENT END header is not 0x10");
}
if (cInt32(bytes, count) != 0x18) {
if (cInt32() != 0x18) {
die("ELEMENT END header chunk is not 0x18 big");
}
int endLineNumber = cInt32(bytes, count);
int comment = cInt32(bytes, count);
int elementNS = cInt32(bytes, count);
int elementName = cInt32(bytes, count);
int endLineNumber = cInt32();
int comment = cInt32();
int elementNS = cInt32();
int elementName = cInt32();
if (currentTag.equals(strings[elementName])) {
writer.add(" />");
wasOneLiner = true;
} else {
writer.startLine("</");
writer.attachSourceLine(endLineNumber);
if (elementNS != -1) {
writer.add(strings[elementNS]).add(':');
}
......@@ -328,26 +345,35 @@ public class BinaryXMLParser {
}
}
private int cInt8(byte[] bytes, int offset) {
byte[] tmp = new byte[4];
tmp[3] = bytes[count++];
return ByteBuffer.wrap(tmp).getInt();
private int cInt8() throws IOException {
return input.read();
}
private int cInt16(byte[] bytes, int offset) {
byte[] tmp = new byte[4];
tmp[3] = bytes[count++];
tmp[2] = bytes[count++];
return ByteBuffer.wrap(tmp).getInt();
private int cInt16() throws IOException {
int b1 = input.read();
int b2 = input.read();
return (b2 & 0xFF) << 8 | (b1 & 0xFF);
}
private int cInt32(byte[] bytes, int offset) {
byte[] tmp = new byte[4];
for (int i = 0; i < 4; i++) {
tmp[3 - i] = bytes[count + i];
private int cInt32() throws IOException {
InputStream in = input;
int b1 = in.read();
int b2 = in.read();
int b3 = in.read();
int b4 = in.read();
return b4 << 24 | (b3 & 0xFF) << 16 | (b2 & 0xFF) << 8 | (b1 & 0xFF);
}
private void readToArray(byte[] arr) throws IOException {
int count = arr.length;
int pos = input.read(arr, 0, count);
while (pos < count) {
int read = input.read(arr, pos, count - pos);
if (read == -1) {
throw new IOException("No data, can't read " + count + " bytes");
}
pos += read;
}
count += 4;
return ByteBuffer.wrap(tmp).getInt();
}
private void die(String message) {
......
......@@ -128,6 +128,11 @@ public abstract class IntegrationTest extends TestUtils {
public int getThreadsCount() {
return 1;
}
@Override
public boolean isSkipResources() {
return true;
}
}, new File(outDir));
}
......
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