From 82d0d622a8e3a588b8916ecdb7b7e224551dc3fb Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 22 Jan 2019 18:13:50 +0300 Subject: [PATCH] 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) --- .../src/main/java/jadx/api/ResourceFile.java | 14 +- .../java/jadx/api/ResourceFileContent.java | 13 +- .../main/java/jadx/api/ResourcesLoader.java | 55 +++---- .../main/java/jadx/core/codegen/NameGen.java | 31 ++-- .../java/jadx/core/dex/nodes/RootNode.java | 12 +- .../src/main/java/jadx/core/utils/Utils.java | 14 ++ .../java/jadx/core/xmlgen/ResContainer.java | 110 +++++--------- .../java/jadx/core/xmlgen/ResTableParser.java | 20 +-- .../main/java/jadx/core/xmlgen/ResXmlGen.java | 2 +- .../java/jadx/core/xmlgen/ResourcesSaver.java | 102 ++++++------- .../src/main/java/jadx/gui/JadxWrapper.java | 5 +- .../java/jadx/gui/treemodel/JResource.java | 138 +++++++++++------- .../main/java/jadx/gui/ui/HeapUsageBar.java | 16 ++ .../src/main/java/jadx/gui/ui/ImagePanel.java | 44 +++++- .../src/main/java/jadx/gui/ui/MainWindow.java | 19 ++- .../java/jadx/gui/ui/codearea/CodePanel.java | 22 ++- .../jadx/gui/ui/codearea/LineNumbers.java | 4 + .../src/main/java/jadx/gui/utils/Utils.java | 13 +- 18 files changed, 342 insertions(+), 292 deletions(-) diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java index 1fdb02885cb..c550f403e5f 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFile.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -35,6 +35,13 @@ public String toString() { private final ResourceType type; 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) { this.decompiler = decompiler; this.name = name; @@ -65,11 +72,4 @@ public ZipRef getZipRef() { public String toString() { 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); - } } diff --git a/jadx-core/src/main/java/jadx/api/ResourceFileContent.java b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java index be6da2b2c54..194c91b2064 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFileContent.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFileContent.java @@ -1,27 +1,18 @@ package jadx.api; import jadx.core.codegen.CodeWriter; -import jadx.core.utils.files.ZipSecurity; import jadx.core.xmlgen.ResContainer; public class ResourceFileContent extends ResourceFile { - private final CodeWriter content; - private ResourceFileContent(String name, ResourceType type, CodeWriter content) { + public ResourceFileContent(String name, ResourceType type, CodeWriter content) { super(null, name, type); this.content = content; } @Override public ResContainer loadContent() { - return ResContainer.singleFile(getName(), content); - } - - public static ResourceFileContent createResourceFileContentInstance(String name, ResourceType type, CodeWriter content) { - if (!ZipSecurity.isValidZipEntryName(name)) { - return null; - } - return new ResourceFileContent(name, type, content); + return ResContainer.textResource(getName(), content); } } diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 054cc181a3e..d93fe5413aa 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -18,6 +18,7 @@ import jadx.api.ResourceFile.ZipRef; import jadx.core.codegen.CodeWriter; import jadx.core.utils.Utils; +import jadx.core.utils.android.Res9patchStreamDecoder; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.files.InputFile; import jadx.core.utils.files.ZipSecurity; @@ -31,8 +32,6 @@ public final class ResourcesLoader { private static final Logger LOG = LoggerFactory.getLogger(ResourcesLoader.class); - private static final int LOAD_SIZE_LIMIT = 10 * 1024 * 1024; - private final JadxDecompiler jadxRef; ResourcesLoader(JadxDecompiler jadxRef) { @@ -47,11 +46,11 @@ List load(List inputFiles) { return list; } - public interface ResourceDecoder { - ResContainer decode(long size, InputStream is) throws IOException; + public interface ResourceDecoder { + T decode(long size, InputStream is) throws IOException; } - public static ResContainer decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { + public static T decodeStream(ResourceFile rf, ResourceDecoder decoder) throws JadxException { try { ZipRef zipRef = rf.getZipRef(); if (zipRef == null) { @@ -80,44 +79,48 @@ public static ResContainer decodeStream(ResourceFile rf, ResourceDecoder decoder static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) { try { - return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is, size)); + return decodeStream(rf, (size, is) -> loadContent(jadxRef, rf, is)); } catch (JadxException e) { LOG.error("Decode error", e); CodeWriter cw = new CodeWriter(); cw.add("Error decode ").add(rf.getType().toString().toLowerCase()); 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, - InputStream inputStream, long size) throws IOException { + InputStream inputStream) throws IOException { switch (rf.getType()) { case MANIFEST: case XML: - return ResContainer.singleFile(rf.getName(), - jadxRef.getXmlParser().parse(inputStream)); + CodeWriter content = jadxRef.getXmlParser().parse(inputStream); + return ResContainer.textResource(rf.getName(), content); case ARSC: - return new ResTableParser() - .decodeFiles(inputStream); + return new ResTableParser().decodeFiles(inputStream); case IMG: - return ResContainer.singleImageFile(rf.getName(), inputStream); - - case CODE: - case LIB: - case FONT: - case UNKNOWN: - return ResContainer.singleBinaryFile(rf.getName(), inputStream); + return decodeImage(rf, inputStream); default: - if (size > LOAD_SIZE_LIMIT) { - 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)); + return ResContainer.resourceFileLink(rf); + } + } + + 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 list, File file) { @@ -141,7 +144,7 @@ private void loadFile(List list, File file) { private void addResourceFile(List list, File file) { String name = file.getAbsolutePath(); ResourceType type = ResourceType.getFileType(name); - ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); + ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type); if (rf != null) { list.add(rf); } @@ -153,7 +156,7 @@ private void addEntry(List list, File zipFile, ZipEntry entry) { } String name = entry.getName(); ResourceType type = ResourceType.getFileType(name); - ResourceFile rf = ResourceFile.createResourceFileInstance(jadxRef, name, type); + ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type); if (rf != null) { rf.setZipRef(new ZipRef(zipFile, name)); list.add(rf); diff --git a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java index 8ae4c46fbb1..665adcbb38b 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java @@ -1,6 +1,5 @@ package jadx.core.codegen; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -21,6 +20,7 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.StringUtils; +import jadx.core.utils.Utils; public class NameGen { @@ -31,20 +31,21 @@ public class NameGen { private final boolean fallback; static { - OBJ_ALIAS = new HashMap<>(); - OBJ_ALIAS.put(Consts.CLASS_STRING, "str"); - OBJ_ALIAS.put(Consts.CLASS_CLASS, "cls"); - OBJ_ALIAS.put(Consts.CLASS_THROWABLE, "th"); - OBJ_ALIAS.put(Consts.CLASS_OBJECT, "obj"); - OBJ_ALIAS.put("java.util.Iterator", "it"); - OBJ_ALIAS.put("java.lang.Boolean", "bool"); - OBJ_ALIAS.put("java.lang.Short", "sh"); - OBJ_ALIAS.put("java.lang.Integer", "num"); - OBJ_ALIAS.put("java.lang.Character", "ch"); - OBJ_ALIAS.put("java.lang.Byte", "b"); - OBJ_ALIAS.put("java.lang.Float", "f"); - OBJ_ALIAS.put("java.lang.Long", "l"); - OBJ_ALIAS.put("java.lang.Double", "d"); + OBJ_ALIAS = Utils.newConstStringMap( + Consts.CLASS_STRING, "str", + Consts.CLASS_CLASS, "cls", + Consts.CLASS_THROWABLE, "th", + Consts.CLASS_OBJECT, "obj", + "java.util.Iterator", "it", + "java.lang.Boolean", "bool", + "java.lang.Short", "sh", + "java.lang.Integer", "num", + "java.lang.Character", "ch", + "java.lang.Byte", "b", + "java.lang.Float", "f", + "java.lang.Long", "l", + "java.lang.Double", "d" + ); } public NameGen(MethodNode mth, boolean fallback) { 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 8598ac872dd..e71cbab6bd6 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 @@ -21,7 +21,6 @@ import jadx.core.utils.ErrorsCounter; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesUtils; -import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.DexFile; import jadx.core.utils.files.InputFile; @@ -81,17 +80,16 @@ public void loadResources(List resources) { LOG.debug("'.arsc' file not found"); return; } - ResTableParser parser = new ResTableParser(); try { - ResourcesLoader.decodeStream(arsc, (size, is) -> { + ResourceStorage resStorage = ResourcesLoader.decodeStream(arsc, (size, is) -> { + ResTableParser parser = new ResTableParser(); parser.decode(is); - return null; + return parser.getResStorage(); }); - } catch (JadxException e) { + processResources(resStorage); + } catch (Exception e) { LOG.error("Failed to parse '.arsc' file", e); - return; } - processResources(parser.getResStorage()); } public void processResources(ResourceStorage resStorage) { diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index be49a4138d6..07267bd97c8 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -5,8 +5,10 @@ import java.io.StringWriter; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.function.Function; import jadx.api.JadxDecompiler; @@ -161,4 +163,16 @@ public static List lockList(List list) { } return new ImmutableList<>(list); } + + public static Map newConstStringMap(String... parameters) { + int len = parameters.length; + if (len == 0) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(len / 2); + for (int i = 0; i < len; i += 2) { + result.put(parameters[i], parameters[i + 1]); + } + return Collections.unmodifiableMap(result); + } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java index 8dfbf2fc1b2..cf8e72772db 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java @@ -1,84 +1,47 @@ 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.InputStream; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; -import org.apache.commons.io.IOUtils; 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.utils.android.Res9patchStreamDecoder; -import jadx.core.utils.exceptions.JadxRuntimeException; public class ResContainer implements Comparable { - 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 Object data; private final List subFiles; - @Nullable - private CodeWriter content; - @Nullable - private BufferedImage image; - @Nullable - private InputStream binary; - - private ResContainer(String name, List 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 textResource(String name, CodeWriter content) { + return new ResContainer(name, Collections.emptyList(), content, DataType.TEXT); } - public static ResContainer singleBinaryFile(String name, InputStream content) { - ResContainer resContainer = new ResContainer(name, Collections.emptyList()); - try { - // TODO: don't store binary files in memory - resContainer.binary = new ByteArrayInputStream(IOUtils.toByteArray(content)); - } catch (Exception e) { - LOG.warn("Contents of the binary resource '{}' not saved, got exception", name, e); - } - return resContainer; + public static ResContainer decodedData(String name, byte[] data) { + return new ResContainer(name, Collections.emptyList(), data, DataType.DECODED_DATA); + } + + public static ResContainer resourceFileLink(ResourceFile resFile) { + return new ResContainer(resFile.getName(), Collections.emptyList(), resFile, DataType.RES_LINK); } - public static ResContainer multiFile(String name) { - return new ResContainer(name, new ArrayList<>()); + public static ResContainer resourceTable(String name, List subFiles, CodeWriter rootContent) { + return new ResContainer(name, subFiles, rootContent, DataType.RES_TABLE); + } + + private ResContainer(String name, List 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() { @@ -89,27 +52,24 @@ public String getFileName() { return name.replace("/", File.separator); } - @Nullable - public CodeWriter getContent() { - return content; + public List getSubFiles() { + return subFiles; } - @Nullable - public InputStream getBinary() { - return binary; + public DataType getDataType() { + return dataType; } - public void setContent(@Nullable CodeWriter content) { - this.content = content; + public CodeWriter getText() { + return (CodeWriter) data; } - @Nullable - public BufferedImage getImage() { - return image; + public byte[] getDecodedData() { + return (byte[]) data; } - public List getSubFiles() { - return subFiles; + public ResourceFile getResLink() { + return (ResourceFile) data; } @Override @@ -136,6 +96,6 @@ public int hashCode() { @Override public String toString() { - return "Res{" + name + ", subFiles=" + subFiles + "}"; + return "Res{" + name + ", type=" + dataType + ", 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 fa8c496b710..dd04ae6c132 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java @@ -66,23 +66,9 @@ public ResContainer decodeFiles(InputStream inputStream) throws IOException { ValuesParser vp = new ValuesParser(strings, resStorage.getResourcesNames()); ResXmlGen resGen = new ResXmlGen(resStorage, vp); - ResContainer res = ResContainer.multiFile("res"); - res.setContent(makeXmlDump()); - res.getSubFiles().addAll(resGen.makeResourcesXml()); - 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; + CodeWriter content = makeXmlDump(); + List xmlFiles = resGen.makeResourcesXml(); + return ResContainer.resourceTable("res", xmlFiles, content); } public CodeWriter makeXmlDump() { diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index 68a8773a4a1..45274480965 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -57,7 +57,7 @@ public List makeResourcesXml() { content.decIndent(); content.startLine(""); content.finish(); - files.add(ResContainer.singleFile(fileName, content)); + files.add(ResContainer.textResource(fileName, content)); } Collections.sort(files); return files; diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java index 4e9145a9140..dd3c129c02e 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -1,25 +1,21 @@ package jadx.core.xmlgen; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; +import java.nio.file.Files; -import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ResourceFile; +import jadx.api.ResourcesLoader; 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.ZipSecurity; -import static jadx.core.utils.files.FileUtils.prepareFile; - public class ResourcesSaver implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(ResourcesSaver.class); @@ -33,76 +29,74 @@ public ResourcesSaver(File outDir, ResourceFile resourceFile) { @Override public void run() { - ResContainer rc = resourceFile.loadContent(); - if (rc != null) { - saveResources(rc); - } + saveResources(resourceFile.loadContent()); } private void saveResources(ResContainer rc) { if (rc == null) { return; } - List subFiles = rc.getSubFiles(); - if (subFiles.isEmpty()) { - save(rc, outDir); - } else { + if (rc.getDataType() == ResContainer.DataType.RES_TABLE) { saveToFile(rc, new File(outDir, "res/values/public.xml")); - for (ResContainer subFile : subFiles) { + for (ResContainer subFile : rc.getSubFiles()) { saveResources(subFile); } + } else { + save(rc, outDir); } } private void save(ResContainer rc, File outDir) { 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)) { - LOG.error("Path traversal attack detected, invalid resource name: {}", - rc.getFileName()); + LOG.error("Path traversal attack detected, invalid resource name: {}", outFile.getPath()); return; } saveToFile(rc, outFile); } private void saveToFile(ResContainer rc, File outFile) { - CodeWriter cw = rc.getContent(); - if (cw != null) { - cw.save(outFile); - return; - } - InputStream binary = rc.getBinary(); - if (binary != null) { - try { + switch (rc.getDataType()) { + case TEXT: + case RES_TABLE: + CodeWriter cw = rc.getText(); + cw.save(outFile); + return; + + 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); - try (FileOutputStream binaryFileStream = new FileOutputStream(outFile)) { - IOUtils.copy(binary, binaryFileStream); - } finally { - binary.close(); + try { + saveResourceFile(resFile, outFile); + } catch (Exception e) { + 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) { - LOG.warn("Resource '{}' not saved, got exception", rc.getName(), e); + throw new JadxRuntimeException("Resource file save error", e); } - return; - } - LOG.warn("Resource '{}' not saved, unknown type", rc.getName()); + return null; + }); } } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index cec9b4fb9ad..11bf8c68c4b 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JavaClass; import jadx.api.JavaPackage; @@ -105,7 +106,7 @@ public File getOpenFile() { return openFile; } - public JadxSettings getSettings() { - return settings; + public JadxArgs getArgs() { + return decompiler.getArgs(); } } 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 73f8b967b20..753af2523ff 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -2,6 +2,7 @@ import javax.swing.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -11,14 +12,13 @@ import jadx.api.ResourceFile; import jadx.api.ResourceFileContent; import jadx.api.ResourceType; +import jadx.api.ResourcesLoader; import jadx.core.codegen.CodeWriter; import jadx.core.xmlgen.ResContainer; import jadx.gui.utils.NLS; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.Utils; -import static jadx.api.ResourceFileContent.createResourceFileContentInstance; - public class JResource extends JLoadableNode implements Comparable { private static final long serialVersionUID = -201018424302612434L; @@ -43,7 +43,7 @@ public enum JResType { private transient boolean loaded; private transient String content; - private transient Map lineMapping; + private transient Map lineMapping = Collections.emptyMap(); public JResource(ResourceFile resFile, String name, JResType type) { this(resFile, name, name, type); @@ -58,15 +58,16 @@ public JResource(ResourceFile resFile, String name, String shortName, JResType t } public final void update() { - removeAllChildren(); - if (!loaded) { + if (files.isEmpty()) { if (type == JResType.DIR || type == JResType.ROOT || 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"))); } } else { - loadContent(); + removeAllChildren(); for (JResource res : files) { res.update(); add(res); @@ -76,13 +77,8 @@ public final void update() { @Override public void loadNode() { - loadContent(); - loaded = true; - update(); - } - - private void loadContent() { getContent(); + update(); } @Override @@ -95,40 +91,68 @@ public List getFiles() { } @Override - public String getContent() { - if (!loaded && resFile != null && type == JResType.FILE) { - loaded = true; - if (isSupportedForView(resFile.getType())) { - ResContainer rc = resFile.loadContent(); - if (rc != null) { - addSubFiles(rc, this, 0); - } + public synchronized String getContent() { + if (loaded) { + return content; + } + if (resFile == null || type != JResType.FILE) { + return null; + } + 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) { - CodeWriter cw = rc.getContent(); - if (cw != null) { - if (depth == 0) { - root.lineMapping = cw.getLineMapping(); - root.content = cw.toString(); - } else { - String resName = rc.getName(); - String[] path = resName.split("/"); - String resShortName = path.length == 0 ? resName : path[path.length - 1]; - ResourceFileContent fileContent = createResourceFileContentInstance(resShortName, ResourceType.XML, cw); - if (fileContent != null) { - addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE)); + private String loadCurrentSingleRes(ResContainer rc) { + switch (rc.getDataType()) { + case TEXT: + case RES_TABLE: + CodeWriter cw = rc.getText(); + lineMapping = cw.getLineMapping(); + return cw.toString(); + + case RES_LINK: + try { + return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> { + if (size > 10 * 1024 * 1024L) { + 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 subFiles = rc.getSubFiles(); - if (!subFiles.isEmpty()) { - for (ResContainer subFile : subFiles) { - addSubFiles(subFile, root, depth + 1); - } + } + + private void loadSubNodes(JResource root, ResContainer rc, int depth) { + 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 String getSyntaxName() { } } + private static final Map 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) { int dot = name.lastIndexOf('.'); if (dot == -1) { return null; } String ext = name.substring(dot + 1); - if (ext.equals("js")) { - 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; + return EXTENSION_TO_FILE_SYNTAX.get(ext); } @Override @@ -256,6 +284,10 @@ public ResourceFile getResFile() { return resFile; } + public Map getLineMapping() { + return lineMapping; + } + @Override public JClass getJParent() { return null; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java index 2881833b128..5155437b1c0 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -4,11 +4,17 @@ import java.awt.*; import java.awt.event.ActionEvent; 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.Utils; 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 double TWO_TO_20 = 1048576d; @@ -32,6 +38,16 @@ public HeapUsageBar() { maxGB = maxKB / TWO_TO_20; update(); 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() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java index d5a8c137ee2..404bf66edb6 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/ImagePanel.java @@ -1,27 +1,57 @@ package jadx.gui.ui; +import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import hu.kazocsaba.imageviewer.ImageViewer; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 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.ui.codearea.CodeArea; public class ImagePanel extends ContentPanel { - private static final long serialVersionUID = 4071356367073142688L; ImagePanel(TabbedPane panel, JResource 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(); - BufferedImage img = resFile.loadContent().getImage(); - ImageViewer imageViewer = new ImageViewer(img); - imageViewer.setZoomFactor(2.); - - setLayout(new BorderLayout()); - add(imageViewer.getComponent()); + ResContainer resContainer = resFile.loadContent(); + ResContainer.DataType dataType = resContainer.getDataType(); + if (dataType == ResContainer.DataType.DECODED_DATA) { + try { + return ImageIO.read(new ByteArrayInputStream(resContainer.getDecodedData())); + } 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 diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index e39470ea71c..c5be06f59b7 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.JadxArgs; import jadx.api.ResourceFile; import jadx.gui.JadxWrapper; import jadx.gui.jobs.BackgroundWorker; @@ -221,12 +222,6 @@ public void reOpenFile() { } private void saveAll(boolean export) { - settings.setExportAsGradleProject(export); - if (export) { - settings.setSkipSources(false); - settings.setSkipResources(false); - } - JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); fileChooser.setToolTipText(NLS.str("file.save_all_msg")); @@ -238,6 +233,15 @@ private void saveAll(boolean export) { int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select")); 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()); ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); progressMonitor.setMillisToPopup(0); @@ -289,6 +293,9 @@ private void toggleDeobfuscation() { private void treeClickAction() { try { Object obj = tree.getLastSelectedPathComponent(); + if (obj == null) { + return; + } if (obj instanceof JResource) { JResource res = (JResource) obj; ResourceFile resFile = res.getResFile(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index aaabef16aab..20de62c770d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -6,13 +6,14 @@ import java.awt.event.InputEvent; import java.awt.event.KeyEvent; +import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.JResource; import jadx.gui.ui.ContentPanel; import jadx.gui.ui.TabbedPane; import jadx.gui.utils.Utils; public final class CodePanel extends ContentPanel { - private static final long serialVersionUID = 5310536092010045565L; private final SearchBar searchBar; @@ -24,7 +25,6 @@ public CodePanel(TabbedPane panel, JNode jnode) { codeArea = new CodeArea(this); searchBar = new SearchBar(codeArea); - scrollPane = new JScrollPane(codeArea); initLineNumbers(); @@ -37,7 +37,23 @@ public CodePanel(TabbedPane panel, JNode jnode) { } 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 { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java index 2b58f9c0739..468e890b992 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/LineNumbers.java @@ -219,4 +219,8 @@ public void caretUpdate(CaretEvent e) { lastLine = currentLine; } } + + public void setUseSourceLines(boolean useSourceLines) { + this.useSourceLines = useSourceLines; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java index 4dadef84524..6dd69da75c4 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -135,18 +135,15 @@ public static boolean isFreeMemoryAvailable() { public static String memoryInfo() { Runtime runtime = Runtime.getRuntime(); - StringBuilder sb = new StringBuilder(); long maxMemory = runtime.maxMemory(); long allocatedMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); - sb.append("heap: ").append(format(allocatedMemory - freeMemory)); - sb.append(", allocated: ").append(format(allocatedMemory)); - sb.append(", free: ").append(format(freeMemory)); - sb.append(", total free: ").append(format(freeMemory + maxMemory - allocatedMemory)); - sb.append(", max: ").append(format(maxMemory)); - - return sb.toString(); + return "heap: " + format(allocatedMemory - freeMemory) + + ", allocated: " + format(allocatedMemory) + + ", free: " + format(freeMemory) + + ", total free: " + format(freeMemory + maxMemory - allocatedMemory) + + ", max: " + format(maxMemory); } private static String format(long mem) {