Commit 82d0d622 authored by Skylot's avatar Skylot

fix: refactor, improve performance and fix some issues in resource processing

fix(gui): instead gradle export was executed normal export
fix(gui): content of some resource files was not shown
perf: direct resource files saving without full length buffer in memory
perf(gui): line numbers will be disabled on big files due to performance issue
feat(gui): click on HeapUsageBar will run GC and update memory info
feat(gui): add more file types for syntax highlights
refactor: ResContainer class changed for support more types of data (added link to resource file)
parent bcaca781
...@@ -35,6 +35,13 @@ public class ResourceFile { ...@@ -35,6 +35,13 @@ public class ResourceFile {
private final ResourceType type; private final ResourceType type;
private ZipRef zipRef; private ZipRef zipRef;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
}
return new ResourceFile(decompiler, name, type);
}
protected ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { protected ResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
this.decompiler = decompiler; this.decompiler = decompiler;
this.name = name; this.name = name;
...@@ -65,11 +72,4 @@ public class ResourceFile { ...@@ -65,11 +72,4 @@ public class ResourceFile {
public String toString() { public String toString() {
return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}"; return "ResourceFile{name='" + name + '\'' + ", type=" + type + "}";
} }
public static ResourceFile createResourceFileInstance(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
}
return new ResourceFile(decompiler, name, type);
}
} }
package jadx.api; package jadx.api;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.utils.files.ZipSecurity;
import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResContainer;
public class ResourceFileContent extends ResourceFile { public class ResourceFileContent extends ResourceFile {
private final CodeWriter content; private final CodeWriter content;
private ResourceFileContent(String name, ResourceType type, CodeWriter content) { public ResourceFileContent(String name, ResourceType type, CodeWriter content) {
super(null, name, type); super(null, name, type);
this.content = content; this.content = content;
} }
@Override @Override
public ResContainer loadContent() { public ResContainer loadContent() {
return ResContainer.singleFile(getName(), content); return ResContainer.textResource(getName(), content);
}
public static ResourceFileContent createResourceFileContentInstance(String name, ResourceType type, CodeWriter content) {
if (!ZipSecurity.isValidZipEntryName(name)) {
return null;
}
return new ResourceFileContent(name, type, content);
} }
} }
...@@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; ...@@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile.ZipRef; import jadx.api.ResourceFile.ZipRef;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.utils.Utils; import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.InputFile; import jadx.core.utils.files.InputFile;
import jadx.core.utils.files.ZipSecurity; import jadx.core.utils.files.ZipSecurity;
...@@ -31,8 +32,6 @@ import static jadx.core.utils.files.FileUtils.copyStream; ...@@ -31,8 +32,6 @@ import static jadx.core.utils.files.FileUtils.copyStream;
public final class ResourcesLoader { public final class ResourcesLoader {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class);
private static final int LOAD_SIZE_LIMIT = 10 * 1024 * 1024;
private final JadxDecompiler jadxRef; private final JadxDecompiler jadxRef;
ResourcesLoader(JadxDecompiler jadxRef) { ResourcesLoader(JadxDecompiler jadxRef) {
...@@ -47,11 +46,11 @@ public final class ResourcesLoader { ...@@ -47,11 +46,11 @@ public final class ResourcesLoader {
return list; return list;
} }
public interface ResourceDecoder { public interface ResourceDecoder<T> {
ResContainer decode(long size, InputStream is) throws IOException; T decode(long size, InputStream is) throws IOException;
} }
public static ResContainer decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) throws JadxException {
try { try {
ZipRef zipRef = rf.getZipRef(); ZipRef zipRef = rf.getZipRef();
if (zipRef == null) { if (zipRef == null) {
...@@ -80,44 +79,48 @@ public final class ResourcesLoader { ...@@ -80,44 +79,48 @@ public final class ResourcesLoader {
static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) { static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {
try { try {
return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is, size)); return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is));
} catch (JadxException e) { } catch (JadxException e) {
LOG.error("Decode error", e); LOG.error("Decode error", e);
CodeWriter cw = new CodeWriter(); CodeWriter cw = new CodeWriter();
cw.add("Error decode ").add(rf.getType().toString().toLowerCase()); cw.add("Error decode ").add(rf.getType().toString().toLowerCase());
cw.startLine(Utils.getStackTrace(e.getCause())); cw.startLine(Utils.getStackTrace(e.getCause()));
return ResContainer.singleFile(rf.getName(), cw); return ResContainer.textResource(rf.getName(), cw);
} }
} }
private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf, private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
InputStream inputStream, long size) throws IOException { InputStream inputStream) throws IOException {
switch (rf.getType()) { switch (rf.getType()) {
case MANIFEST: case MANIFEST:
case XML: case XML:
return ResContainer.singleFile(rf.getName(), CodeWriter content = jadxRef.getXmlParser().parse(inputStream);
jadxRef.getXmlParser().parse(inputStream)); return ResContainer.textResource(rf.getName(), content);
case ARSC: case ARSC:
return new ResTableParser() return new ResTableParser().decodeFiles(inputStream);
.decodeFiles(inputStream);
case IMG: case IMG:
return ResContainer.singleImageFile(rf.getName(), inputStream); return decodeImage(rf, inputStream);
case CODE:
case LIB:
case FONT:
case UNKNOWN:
return ResContainer.singleBinaryFile(rf.getName(), inputStream);
default: default:
if (size > LOAD_SIZE_LIMIT) { return ResContainer.resourceFileLink(rf);
return ResContainer.singleFile(rf.getName(), }
new CodeWriter().add("File too big, size: " + String.format("%.2f KB", size / 1024.))); }
}
return ResContainer.singleFile(rf.getName(), loadToCodeWriter(inputStream)); private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getName();
if (name.endsWith(".9.png")) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
decoder.decode(inputStream, os);
return ResContainer.decodedData(rf.getName(), os.toByteArray());
} catch (Exception e) {
LOG.error("Failed to decode 9-patch png image, path: {}", name, e);
}
} }
return ResContainer.resourceFileLink(rf);
} }
private void loadFile(List<ResourceFile> list, File file) { private void loadFile(List<ResourceFile> list, File file) {
...@@ -141,7 +144,7 @@ public final class ResourcesLoader { ...@@ -141,7 +144,7 @@ public final class ResourcesLoader {
private void addResourceFile(List<ResourceFile> list, File file) { private void addResourceFile(List<ResourceFile> list, File file) {
String name = file.getAbsolutePath(); String name = file.getAbsolutePath();
ResourceType type = ResourceType.getFileType(name); ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type);
if (rf != null) { if (rf != null) {
list.add(rf); list.add(rf);
} }
...@@ -153,7 +156,7 @@ public final class ResourcesLoader { ...@@ -153,7 +156,7 @@ public final class ResourcesLoader {
} }
String name = entry.getName(); String name = entry.getName();
ResourceType type = ResourceType.getFileType(name); ResourceType type = ResourceType.getFileType(name);
ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type);
if (rf != null) { if (rf != null) {
rf.setZipRef(new ZipRef(zipFile, name)); rf.setZipRef(new ZipRef(zipFile, name));
list.add(rf); list.add(rf);
......
package jadx.core.codegen; package jadx.core.codegen;
import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
...@@ -21,6 +20,7 @@ import jadx.core.dex.instructions.mods.ConstructorInsn; ...@@ -21,6 +20,7 @@ import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.StringUtils; import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
public class NameGen { public class NameGen {
...@@ -31,20 +31,21 @@ public class NameGen { ...@@ -31,20 +31,21 @@ public class NameGen {
private final boolean fallback; private final boolean fallback;
static { static {
OBJ_ALIAS = new HashMap<>(); OBJ_ALIAS = Utils.newConstStringMap(
OBJ_ALIAS.put(Consts.CLASS_STRING, "str"); Consts.CLASS_STRING, "str",
OBJ_ALIAS.put(Consts.CLASS_CLASS, "cls"); Consts.CLASS_CLASS, "cls",
OBJ_ALIAS.put(Consts.CLASS_THROWABLE, "th"); Consts.CLASS_THROWABLE, "th",
OBJ_ALIAS.put(Consts.CLASS_OBJECT, "obj"); Consts.CLASS_OBJECT, "obj",
OBJ_ALIAS.put("java.util.Iterator", "it"); "java.util.Iterator", "it",
OBJ_ALIAS.put("java.lang.Boolean", "bool"); "java.lang.Boolean", "bool",
OBJ_ALIAS.put("java.lang.Short", "sh"); "java.lang.Short", "sh",
OBJ_ALIAS.put("java.lang.Integer", "num"); "java.lang.Integer", "num",
OBJ_ALIAS.put("java.lang.Character", "ch"); "java.lang.Character", "ch",
OBJ_ALIAS.put("java.lang.Byte", "b"); "java.lang.Byte", "b",
OBJ_ALIAS.put("java.lang.Float", "f"); "java.lang.Float", "f",
OBJ_ALIAS.put("java.lang.Long", "l"); "java.lang.Long", "l",
OBJ_ALIAS.put("java.lang.Double", "d"); "java.lang.Double", "d"
);
} }
public NameGen(MethodNode mth, boolean fallback) { public NameGen(MethodNode mth, boolean fallback) {
......
...@@ -21,7 +21,6 @@ import jadx.core.dex.info.MethodInfo; ...@@ -21,7 +21,6 @@ import jadx.core.dex.info.MethodInfo;
import jadx.core.utils.ErrorsCounter; import jadx.core.utils.ErrorsCounter;
import jadx.core.utils.StringUtils; import jadx.core.utils.StringUtils;
import jadx.core.utils.android.AndroidResourcesUtils; import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.DexFile; import jadx.core.utils.files.DexFile;
import jadx.core.utils.files.InputFile; import jadx.core.utils.files.InputFile;
...@@ -81,17 +80,16 @@ public class RootNode { ...@@ -81,17 +80,16 @@ public class RootNode {
LOG.debug("'.arsc' file not found"); LOG.debug("'.arsc' file not found");
return; return;
} }
ResTableParser parser = new ResTableParser();
try { try {
ResourcesLoader.decodeStream(arsc, (size, is) -> { ResourceStorage resStorage = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser parser = new ResTableParser();
parser.decode(is); parser.decode(is);
return null; return parser.getResStorage();
}); });
} catch (JadxException e) { processResources(resStorage);
} catch (Exception e) {
LOG.error("Failed to parse '.arsc' file", e); LOG.error("Failed to parse '.arsc' file", e);
return;
} }
processResources(parser.getResStorage());
} }
public void processResources(ResourceStorage resStorage) { public void processResources(ResourceStorage resStorage) {
......
...@@ -5,8 +5,10 @@ import java.io.PrintWriter; ...@@ -5,8 +5,10 @@ import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import jadx.api.JadxDecompiler; import jadx.api.JadxDecompiler;
...@@ -161,4 +163,16 @@ public class Utils { ...@@ -161,4 +163,16 @@ public class Utils {
} }
return new ImmutableList<>(list); return new ImmutableList<>(list);
} }
public static Map<String, String> newConstStringMap(String... parameters) {
int len = parameters.length;
if (len == 0) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<>(len / 2);
for (int i = 0; i < len; i += 2) {
result.put(parameters[i], parameters[i + 1]);
}
return Collections.unmodifiableMap(result);
}
} }
package jadx.core.xmlgen; package jadx.core.xmlgen;
import javax.imageio.ImageIO;
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.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ResContainer implements Comparable<ResContainer> { public class ResContainer implements Comparable<ResContainer> {
private static final Logger LOG = LoggerFactory.getLogger(ResContainer.class); public enum DataType {
TEXT, DECODED_DATA, RES_LINK, RES_TABLE
}
private final DataType dataType;
private final String name; private final String name;
private final Object data;
private final List<ResContainer> subFiles; private final List<ResContainer> subFiles;
@Nullable public static ResContainer textResource(String name, CodeWriter content) {
private CodeWriter content; return new ResContainer(name, Collections.emptyList(), content, DataType.TEXT);
@Nullable
private BufferedImage image;
@Nullable
private InputStream binary;
private ResContainer(String name, List<ResContainer> subFiles) {
this.name = name;
this.subFiles = subFiles;
}
public static ResContainer singleFile(String name, CodeWriter content) {
ResContainer resContainer = new ResContainer(name, Collections.emptyList());
resContainer.content = content;
return resContainer;
}
public static ResContainer singleImageFile(String name, InputStream content) {
ResContainer resContainer = new ResContainer(name, Collections.emptyList());
InputStream newContent = content;
if (name.endsWith(".9.png")) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
decoder.decode(content, os);
} catch (Exception e) {
LOG.error("Failed to decode 9-patch png image, path: {}", name, e);
}
newContent = new ByteArrayInputStream(os.toByteArray());
}
try {
resContainer.image = ImageIO.read(newContent);
} catch (Exception e) {
throw new JadxRuntimeException("Image load error", e);
}
return resContainer;
} }
public static ResContainer singleBinaryFile(String name, InputStream content) { public static ResContainer decodedData(String name, byte[] data) {
ResContainer resContainer = new ResContainer(name, Collections.emptyList()); return new ResContainer(name, Collections.emptyList(), data, DataType.DECODED_DATA);
try { }
// TODO: don't store binary files in memory
resContainer.binary = new ByteArrayInputStream(IOUtils.toByteArray(content)); public static ResContainer resourceFileLink(ResourceFile resFile) {
} catch (Exception e) { return new ResContainer(resFile.getName(), Collections.emptyList(), resFile, DataType.RES_LINK);
LOG.warn("Contents of the binary resource '{}' not saved, got exception", name, e);
}
return resContainer;
} }
public static ResContainer multiFile(String name) { public static ResContainer resourceTable(String name, List<ResContainer> subFiles, CodeWriter rootContent) {
return new ResContainer(name, new ArrayList<>()); return new ResContainer(name, subFiles, rootContent, DataType.RES_TABLE);
}
private ResContainer(String name, List<ResContainer> subFiles, Object data, DataType dataType) {
this.name = Objects.requireNonNull(name);
this.subFiles = Objects.requireNonNull(subFiles);
this.data = Objects.requireNonNull(data);
this.dataType = Objects.requireNonNull(dataType);
} }
public String getName() { public String getName() {
...@@ -89,27 +52,24 @@ public class ResContainer implements Comparable<ResContainer> { ...@@ -89,27 +52,24 @@ public class ResContainer implements Comparable<ResContainer> {
return name.replace("/", File.separator); return name.replace("/", File.separator);
} }
@Nullable public List<ResContainer> getSubFiles() {
public CodeWriter getContent() { return subFiles;
return content;
} }
@Nullable public DataType getDataType() {
public InputStream getBinary() { return dataType;
return binary;
} }
public void setContent(@Nullable CodeWriter content) { public CodeWriter getText() {
this.content = content; return (CodeWriter) data;
} }
@Nullable public byte[] getDecodedData() {
public BufferedImage getImage() { return (byte[]) data;
return image;
} }
public List<ResContainer> getSubFiles() { public ResourceFile getResLink() {
return subFiles; return (ResourceFile) data;
} }
@Override @Override
...@@ -136,6 +96,6 @@ public class ResContainer implements Comparable<ResContainer> { ...@@ -136,6 +96,6 @@ public class ResContainer implements Comparable<ResContainer> {
@Override @Override
public String toString() { public String toString() {
return "Res{" + name + ", subFiles=" + subFiles + "}"; return "Res{" + name + ", type=" + dataType + ", subFiles=" + subFiles + "}";
} }
} }
...@@ -66,23 +66,9 @@ public class ResTableParser extends CommonBinaryParser { ...@@ -66,23 +66,9 @@ public class ResTableParser extends CommonBinaryParser {
ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames());
ResXmlGen resGen = new ResXmlGen(resStorage, vp); ResXmlGen resGen = new ResXmlGen(resStorage, vp);
ResContainer res = ResContainer.multiFile("res"); CodeWriter content = makeXmlDump();
res.setContent(makeXmlDump()); List<ResContainer> xmlFiles = resGen.makeResourcesXml();
res.getSubFiles().addAll(resGen.makeResourcesXml()); return ResContainer.resourceTable("res", xmlFiles, content);
return res;
}
public CodeWriter makeDump() {
CodeWriter writer = new CodeWriter();
writer.add("app package: ").add(resStorage.getAppPackage());
writer.startLine();
ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames());
for (ResourceEntry ri : resStorage.getResources()) {
writer.startLine(ri + ": " + vp.getValueString(ri));
}
writer.finish();
return writer;
} }
public CodeWriter makeXmlDump() { public CodeWriter makeXmlDump() {
......
...@@ -57,7 +57,7 @@ public class ResXmlGen { ...@@ -57,7 +57,7 @@ public class ResXmlGen {
content.decIndent(); content.decIndent();
content.startLine("</resources>"); content.startLine("</resources>");
content.finish(); content.finish();
files.add(ResContainer.singleFile(fileName, content)); files.add(ResContainer.textResource(fileName, content));
} }
Collections.sort(files); Collections.sort(files);
return files; return files;
......
package jadx.core.xmlgen; package jadx.core.xmlgen;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.nio.file.Files;
import java.io.InputStream;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile; import jadx.api.ResourceFile;
import jadx.api.ResourcesLoader;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipSecurity; import jadx.core.utils.files.ZipSecurity;
import static jadx.core.utils.files.FileUtils.prepareFile;
public class ResourcesSaver implements Runnable { public class ResourcesSaver implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(ResourcesSaver.class); private static final Logger LOG = LoggerFactory.getLogger(ResourcesSaver.class);
...@@ -33,76 +29,74 @@ public class ResourcesSaver implements Runnable { ...@@ -33,76 +29,74 @@ public class ResourcesSaver implements Runnable {
@Override @Override
public void run() { public void run() {
ResContainer rc = resourceFile.loadContent(); saveResources(resourceFile.loadContent());
if (rc != null) {
saveResources(rc);
}
} }
private void saveResources(ResContainer rc) { private void saveResources(ResContainer rc) {
if (rc == null) { if (rc == null) {
return; return;
} }
List<ResContainer> subFiles = rc.getSubFiles(); if (rc.getDataType() == ResContainer.DataType.RES_TABLE) {
if (subFiles.isEmpty()) {
save(rc, outDir);
} else {
saveToFile(rc, new File(outDir, "res/values/public.xml")); saveToFile(rc, new File(outDir, "res/values/public.xml"));
for (ResContainer subFile : subFiles) { for (ResContainer subFile : rc.getSubFiles()) {
saveResources(subFile); saveResources(subFile);
} }
} else {
save(rc, outDir);
} }
} }
private void save(ResContainer rc, File outDir) { private void save(ResContainer rc, File outDir) {
File outFile = new File(outDir, rc.getFileName()); File outFile = new File(outDir, rc.getFileName());
BufferedImage image = rc.getImage();
if (image != null) {
String ext = FilenameUtils.getExtension(outFile.getName());
try {
outFile = prepareFile(outFile);
if (!ZipSecurity.isInSubDirectory(outDir, outFile)) {
LOG.error("Path traversal attack detected, invalid resource name: {}",
outFile.getPath());
return;
}
ImageIO.write(image, ext, outFile);
} catch (IOException e) {
LOG.error("Failed to save image: {}", rc.getName(), e);
}
return;
}
if (!ZipSecurity.isInSubDirectory(outDir, outFile)) { if (!ZipSecurity.isInSubDirectory(outDir, outFile)) {
LOG.error("Path traversal attack detected, invalid resource name: {}", LOG.error("Path traversal attack detected, invalid resource name: {}", outFile.getPath());
rc.getFileName());
return; return;
} }
saveToFile(rc, outFile); saveToFile(rc, outFile);
} }
private void saveToFile(ResContainer rc, File outFile) { private void saveToFile(ResContainer rc, File outFile) {
CodeWriter cw = rc.getContent(); switch (rc.getDataType()) {
if (cw != null) { case TEXT:
cw.save(outFile); case RES_TABLE:
return; CodeWriter cw = rc.getText();
} cw.save(outFile);
InputStream binary = rc.getBinary(); return;
if (binary != null) {
try { case DECODED_DATA:
byte[] data = rc.getDecodedData();
FileUtils.makeDirsForFile(outFile);
try {
Files.write(outFile.toPath(), data);
} catch (Exception e) {
LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e);
}
return;
case RES_LINK:
ResourceFile resFile = rc.getResLink();
FileUtils.makeDirsForFile(outFile); FileUtils.makeDirsForFile(outFile);
try (FileOutputStream binaryFileStream = new FileOutputStream(outFile)) { try {
IOUtils.copy(binary, binaryFileStream); saveResourceFile(resFile, outFile);
} finally { } catch (Exception e) {
binary.close(); LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e);
} }
return;
default:
LOG.warn("Resource '{}' not saved, unknown type", rc.getName());
break;
}
}
private void saveResourceFile(ResourceFile resFile, File outFile) throws JadxException {
ResourcesLoader.decodeStream(resFile, (size, is) -> {
try (FileOutputStream fileStream = new FileOutputStream(outFile)) {
IOUtils.copy(is, fileStream);
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); throw new JadxRuntimeException("Resource file save error", e);
} }
return; return null;
} });
LOG.warn("Resource '{}' not saved, unknown type", rc.getName());
} }
} }
...@@ -10,6 +10,7 @@ import java.util.stream.Collectors; ...@@ -10,6 +10,7 @@ import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler; import jadx.api.JadxDecompiler;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.api.JavaPackage; import jadx.api.JavaPackage;
...@@ -105,7 +106,7 @@ public class JadxWrapper { ...@@ -105,7 +106,7 @@ public class JadxWrapper {
return openFile; return openFile;
} }
public JadxSettings getSettings() { public JadxArgs getArgs() {
return settings; return decompiler.getArgs();
} }
} }
...@@ -2,6 +2,7 @@ package jadx.gui.treemodel; ...@@ -2,6 +2,7 @@ package jadx.gui.treemodel;
import javax.swing.*; import javax.swing.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -11,14 +12,13 @@ import org.jetbrains.annotations.NotNull; ...@@ -11,14 +12,13 @@ import org.jetbrains.annotations.NotNull;
import jadx.api.ResourceFile; import jadx.api.ResourceFile;
import jadx.api.ResourceFileContent; import jadx.api.ResourceFileContent;
import jadx.api.ResourceType; import jadx.api.ResourceType;
import jadx.api.ResourcesLoader;
import jadx.core.codegen.CodeWriter; import jadx.core.codegen.CodeWriter;
import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResContainer;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.OverlayIcon;
import jadx.gui.utils.Utils; import jadx.gui.utils.Utils;
import static jadx.api.ResourceFileContent.createResourceFileContentInstance;
public class JResource extends JLoadableNode implements Comparable<JResource> { public class JResource extends JLoadableNode implements Comparable<JResource> {
private static final long serialVersionUID = -201018424302612434L; private static final long serialVersionUID = -201018424302612434L;
...@@ -43,7 +43,7 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -43,7 +43,7 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
private transient boolean loaded; private transient boolean loaded;
private transient String content; private transient String content;
private transient Map<Integer, Integer> lineMapping; private transient Map<Integer, Integer> lineMapping = Collections.emptyMap();
public JResource(ResourceFile resFile, String name, JResType type) { public JResource(ResourceFile resFile, String name, JResType type) {
this(resFile, name, name, type); this(resFile, name, name, type);
...@@ -58,15 +58,16 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -58,15 +58,16 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
} }
public final void update() { public final void update() {
removeAllChildren(); if (files.isEmpty()) {
if (!loaded) {
if (type == JResType.DIR if (type == JResType.DIR
|| type == JResType.ROOT || type == JResType.ROOT
|| resFile.getType() == ResourceType.ARSC) { || resFile.getType() == ResourceType.ARSC) {
// fake leaf to force show expand button
// real sub nodes will load on expand in loadNode() method
add(new TextNode(NLS.str("tree.loading"))); add(new TextNode(NLS.str("tree.loading")));
} }
} else { } else {
loadContent(); removeAllChildren();
for (JResource res : files) { for (JResource res : files) {
res.update(); res.update();
add(res); add(res);
...@@ -76,13 +77,8 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -76,13 +77,8 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
@Override @Override
public void loadNode() { public void loadNode() {
loadContent();
loaded = true;
update();
}
private void loadContent() {
getContent(); getContent();
update();
} }
@Override @Override
...@@ -95,40 +91,68 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -95,40 +91,68 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
} }
@Override @Override
public String getContent() { public synchronized String getContent() {
if (!loaded && resFile != null && type == JResType.FILE) { if (loaded) {
loaded = true; return content;
if (isSupportedForView(resFile.getType())) { }
ResContainer rc = resFile.loadContent(); if (resFile == null || type != JResType.FILE) {
if (rc != null) { return null;
addSubFiles(rc, this, 0); }
} if (!isSupportedForView(resFile.getType())) {
return null;
}
ResContainer rc = resFile.loadContent();
if (rc == null) {
return null;
}
if (rc.getDataType() == ResContainer.DataType.RES_TABLE) {
content = loadCurrentSingleRes(rc);
for (ResContainer subFile : rc.getSubFiles()) {
loadSubNodes(this, subFile, 1);
} }
loaded = true;
return content;
} }
return content; // single node
return loadCurrentSingleRes(rc);
} }
private void addSubFiles(ResContainer rc, JResource root, int depth) { private String loadCurrentSingleRes(ResContainer rc) {
CodeWriter cw = rc.getContent(); switch (rc.getDataType()) {
if (cw != null) { case TEXT:
if (depth == 0) { case RES_TABLE:
root.lineMapping = cw.getLineMapping(); CodeWriter cw = rc.getText();
root.content = cw.toString(); lineMapping = cw.getLineMapping();
} else { return cw.toString();
String resName = rc.getName();
String[] path = resName.split("/"); case RES_LINK:
String resShortName = path.length == 0 ? resName : path[path.length - 1]; try {
ResourceFileContent fileContent = createResourceFileContentInstance(resShortName, ResourceType.XML, cw); return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> {
if (fileContent != null) { if (size > 10 * 1024 * 1024L) {
addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE)); return "File too large for view";
}
return ResourcesLoader.loadToCodeWriter(is).toString();
});
} catch (Exception e) {
return "Failed to load resource file: \n" + jadx.core.utils.Utils.getStackTrace(e);
} }
}
case DECODED_DATA:
default:
return "Unexpected resource type: " + rc;
} }
List<ResContainer> subFiles = rc.getSubFiles(); }
if (!subFiles.isEmpty()) {
for (ResContainer subFile : subFiles) { private void loadSubNodes(JResource root, ResContainer rc, int depth) {
addSubFiles(subFile, root, depth + 1); String resName = rc.getName();
} String[] path = resName.split("/");
String resShortName = path.length == 0 ? resName : path[path.length - 1];
CodeWriter cw = rc.getText();
ResourceFileContent fileContent = new ResourceFileContent(resShortName, ResourceType.XML, cw);
addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE));
for (ResContainer subFile : rc.getSubFiles()) {
loadSubNodes(root, subFile, depth + 1);
} }
} }
...@@ -190,25 +214,29 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -190,25 +214,29 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
} }
} }
private static final Map<String, String> EXTENSION_TO_FILE_SYNTAX = jadx.core.utils.Utils.newConstStringMap(
"java", SyntaxConstants.SYNTAX_STYLE_JAVA,
"js", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT,
"ts", SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT,
"json", SyntaxConstants.SYNTAX_STYLE_JSON,
"css", SyntaxConstants.SYNTAX_STYLE_CSS,
"less", SyntaxConstants.SYNTAX_STYLE_LESS,
"html", SyntaxConstants.SYNTAX_STYLE_HTML,
"xml", SyntaxConstants.SYNTAX_STYLE_XML,
"yaml", SyntaxConstants.SYNTAX_STYLE_YAML,
"properties", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE,
"ini", SyntaxConstants.SYNTAX_STYLE_INI,
"sql", SyntaxConstants.SYNTAX_STYLE_SQL,
"arsc", SyntaxConstants.SYNTAX_STYLE_XML
);
private String getSyntaxByExtension(String name) { private String getSyntaxByExtension(String name) {
int dot = name.lastIndexOf('.'); int dot = name.lastIndexOf('.');
if (dot == -1) { if (dot == -1) {
return null; return null;
} }
String ext = name.substring(dot + 1); String ext = name.substring(dot + 1);
if (ext.equals("js")) { return EXTENSION_TO_FILE_SYNTAX.get(ext);
return SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT;
}
if (ext.equals("css")) {
return SyntaxConstants.SYNTAX_STYLE_CSS;
}
if (ext.equals("html")) {
return SyntaxConstants.SYNTAX_STYLE_HTML;
}
if (ext.equals("arsc")) {
return SyntaxConstants.SYNTAX_STYLE_XML;
}
return null;
} }
@Override @Override
...@@ -256,6 +284,10 @@ public class JResource extends JLoadableNode implements Comparable<JResource> { ...@@ -256,6 +284,10 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
return resFile; return resFile;
} }
public Map<Integer, Integer> getLineMapping() {
return lineMapping;
}
@Override @Override
public JClass getJParent() { public JClass getJParent() {
return null; return null;
......
...@@ -4,11 +4,17 @@ import javax.swing.*; ...@@ -4,11 +4,17 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.Utils; import jadx.gui.utils.Utils;
public class HeapUsageBar extends JProgressBar implements ActionListener { public class HeapUsageBar extends JProgressBar implements ActionListener {
private static final Logger LOG = LoggerFactory.getLogger(HeapUsageBar.class);
private static final long serialVersionUID = -8739563124249884967L; private static final long serialVersionUID = -8739563124249884967L;
private static final double TWO_TO_20 = 1048576d; private static final double TWO_TO_20 = 1048576d;
...@@ -32,6 +38,16 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { ...@@ -32,6 +38,16 @@ public class HeapUsageBar extends JProgressBar implements ActionListener {
maxGB = maxKB / TWO_TO_20; maxGB = maxKB / TWO_TO_20;
update(); update();
timer = new Timer(2000, this); timer = new Timer(2000, this);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
Runtime.getRuntime().gc();
update();
if (LOG.isDebugEnabled()) {
LOG.debug("Memory used: {}", Utils.memoryInfo());
}
}
});
} }
public void update() { public void update() {
......
package jadx.gui.ui; package jadx.gui.ui;
import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import hu.kazocsaba.imageviewer.ImageViewer; import hu.kazocsaba.imageviewer.ImageViewer;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import jadx.api.ResourceFile; import jadx.api.ResourceFile;
import jadx.api.ResourcesLoader;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.treemodel.JResource; import jadx.gui.treemodel.JResource;
import jadx.gui.ui.codearea.CodeArea;
public class ImagePanel extends ContentPanel { public class ImagePanel extends ContentPanel {
private static final long serialVersionUID = 4071356367073142688L; private static final long serialVersionUID = 4071356367073142688L;
ImagePanel(TabbedPane panel, JResource res) { ImagePanel(TabbedPane panel, JResource res) {
super(panel, res); super(panel, res);
setLayout(new BorderLayout());
try {
BufferedImage img = loadImage(res);
ImageViewer imageViewer = new ImageViewer(img);
add(imageViewer.getComponent());
} catch (Exception e) {
RSyntaxTextArea textArea = CodeArea.getDefaultArea(panel.getMainWindow());
textArea.setText("Image load error: \n" + Utils.getStackTrace(e));
add(textArea);
}
}
private BufferedImage loadImage(JResource res) {
ResourceFile resFile = res.getResFile(); ResourceFile resFile = res.getResFile();
BufferedImage img = resFile.loadContent().getImage(); ResContainer resContainer = resFile.loadContent();
ImageViewer imageViewer = new ImageViewer(img); ResContainer.DataType dataType = resContainer.getDataType();
imageViewer.setZoomFactor(2.); if (dataType == ResContainer.DataType.DECODED_DATA) {
try {
setLayout(new BorderLayout()); return ImageIO.read(new ByteArrayInputStream(resContainer.getDecodedData()));
add(imageViewer.getComponent()); } catch (Exception e) {
throw new JadxRuntimeException("Failed to load image", e);
}
} else if (dataType == ResContainer.DataType.RES_LINK) {
try {
return ResourcesLoader.decodeStream(resFile, (size, is) -> ImageIO.read(is));
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load image", e);
}
} else {
throw new JadxRuntimeException("Unsupported resource image data type: " + resFile);
}
} }
@Override @Override
......
...@@ -31,6 +31,7 @@ import org.fife.ui.rsyntaxtextarea.Theme; ...@@ -31,6 +31,7 @@ import org.fife.ui.rsyntaxtextarea.Theme;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import jadx.api.JadxArgs;
import jadx.api.ResourceFile; import jadx.api.ResourceFile;
import jadx.gui.JadxWrapper; import jadx.gui.JadxWrapper;
import jadx.gui.jobs.BackgroundWorker; import jadx.gui.jobs.BackgroundWorker;
...@@ -221,12 +222,6 @@ public class MainWindow extends JFrame { ...@@ -221,12 +222,6 @@ public class MainWindow extends JFrame {
} }
private void saveAll(boolean export) { private void saveAll(boolean export) {
settings.setExportAsGradleProject(export);
if (export) {
settings.setSkipSources(false);
settings.setSkipResources(false);
}
JFileChooser fileChooser = new JFileChooser(); JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fileChooser.setToolTipText(NLS.str("file.save_all_msg")); fileChooser.setToolTipText(NLS.str("file.save_all_msg"));
...@@ -238,6 +233,15 @@ public class MainWindow extends JFrame { ...@@ -238,6 +233,15 @@ public class MainWindow extends JFrame {
int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select")); int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select"));
if (ret == JFileChooser.APPROVE_OPTION) { if (ret == JFileChooser.APPROVE_OPTION) {
JadxArgs decompilerArgs = wrapper.getArgs();
decompilerArgs.setExportAsGradleProject(export);
if (export) {
decompilerArgs.setSkipSources(false);
decompilerArgs.setSkipResources(false);
} else {
decompilerArgs.setSkipSources(settings.isSkipSources());
decompilerArgs.setSkipResources(settings.isSkipResources());
}
settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().getPath()); settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().getPath());
ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100);
progressMonitor.setMillisToPopup(0); progressMonitor.setMillisToPopup(0);
...@@ -289,6 +293,9 @@ public class MainWindow extends JFrame { ...@@ -289,6 +293,9 @@ public class MainWindow extends JFrame {
private void treeClickAction() { private void treeClickAction() {
try { try {
Object obj = tree.getLastSelectedPathComponent(); Object obj = tree.getLastSelectedPathComponent();
if (obj == null) {
return;
}
if (obj instanceof JResource) { if (obj instanceof JResource) {
JResource res = (JResource) obj; JResource res = (JResource) obj;
ResourceFile resFile = res.getResFile(); ResourceFile resFile = res.getResFile();
......
...@@ -6,13 +6,14 @@ import java.awt.event.ActionEvent; ...@@ -6,13 +6,14 @@ import java.awt.event.ActionEvent;
import java.awt.event.InputEvent; import java.awt.event.InputEvent;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.ContentPanel; import jadx.gui.ui.ContentPanel;
import jadx.gui.ui.TabbedPane; import jadx.gui.ui.TabbedPane;
import jadx.gui.utils.Utils; import jadx.gui.utils.Utils;
public final class CodePanel extends ContentPanel { public final class CodePanel extends ContentPanel {
private static final long serialVersionUID = 5310536092010045565L; private static final long serialVersionUID = 5310536092010045565L;
private final SearchBar searchBar; private final SearchBar searchBar;
...@@ -24,7 +25,6 @@ public final class CodePanel extends ContentPanel { ...@@ -24,7 +25,6 @@ public final class CodePanel extends ContentPanel {
codeArea = new CodeArea(this); codeArea = new CodeArea(this);
searchBar = new SearchBar(codeArea); searchBar = new SearchBar(codeArea);
scrollPane = new JScrollPane(codeArea); scrollPane = new JScrollPane(codeArea);
initLineNumbers(); initLineNumbers();
...@@ -37,7 +37,23 @@ public final class CodePanel extends ContentPanel { ...@@ -37,7 +37,23 @@ public final class CodePanel extends ContentPanel {
} }
private void initLineNumbers() { private void initLineNumbers() {
scrollPane.setRowHeaderView(new LineNumbers(codeArea)); // TODO: fix slow line rendering on big files
if (codeArea.getDocument().getLength() <= 100_000) {
LineNumbers numbers = new LineNumbers(codeArea);
numbers.setUseSourceLines(isUseSourceLines());
scrollPane.setRowHeaderView(numbers);
}
}
private boolean isUseSourceLines() {
if (node instanceof JClass) {
return true;
}
if (node instanceof JResource) {
JResource resNode = (JResource) node;
return !resNode.getLineMapping().isEmpty();
}
return false;
} }
private class SearchAction extends AbstractAction { private class SearchAction extends AbstractAction {
......
...@@ -219,4 +219,8 @@ public class LineNumbers extends JPanel implements CaretListener { ...@@ -219,4 +219,8 @@ public class LineNumbers extends JPanel implements CaretListener {
lastLine = currentLine; lastLine = currentLine;
} }
} }
public void setUseSourceLines(boolean useSourceLines) {
this.useSourceLines = useSourceLines;
}
} }
...@@ -135,18 +135,15 @@ public class Utils { ...@@ -135,18 +135,15 @@ public class Utils {
public static String memoryInfo() { public static String memoryInfo() {
Runtime runtime = Runtime.getRuntime(); Runtime runtime = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
long maxMemory = runtime.maxMemory(); long maxMemory = runtime.maxMemory();
long allocatedMemory = runtime.totalMemory(); long allocatedMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory(); long freeMemory = runtime.freeMemory();
sb.append("heap: ").append(format(allocatedMemory - freeMemory)); return "heap: " + format(allocatedMemory - freeMemory) +
sb.append(", allocated: ").append(format(allocatedMemory)); ", allocated: " + format(allocatedMemory) +
sb.append(", free: ").append(format(freeMemory)); ", free: " + format(freeMemory) +
sb.append(", total free: ").append(format(freeMemory + maxMemory - allocatedMemory)); ", total free: " + format(freeMemory + maxMemory - allocatedMemory) +
sb.append(", max: ").append(format(maxMemory)); ", max: " + format(maxMemory);
return sb.toString();
} }
private static String format(long mem) { private static String format(long mem) {
......
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