From 9ea3f0f2405d7acac7dfb9b357442bb2effbf487 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 20 Aug 2021 20:59:30 +0300 Subject: [PATCH] fix: support 'swap' and 'wide' opcodes, other fixes for java-input --- jadx-core/build.gradle | 5 +- .../main/java/jadx/core/codegen/InsnGen.java | 1 - .../java/jadx/core/dex/info/AccessInfo.java | 7 ++ .../java/jadx/core/dex/nodes/ClassNode.java | 49 ++++++---- .../java/jadx/core/dex/nodes/RootNode.java | 30 ++++-- .../jadx/tests/external/BaseExternalTest.java | 11 ++- .../integration/others/TestJavaSwap.java | 95 +++++++++++++++++++ .../input/dex/sections/DexClassData.java | 6 +- .../input/javaconvert/JavaConvertLoader.java | 38 +------- .../plugins/input/java/JavaFileLoader.java | 52 +++++----- .../input/java/data/ConstPoolReader.java | 11 ++- .../input/java/data/JavaClassData.java | 6 +- .../data/attributes/AttributesReader.java | 6 +- .../java/data/attributes/JavaAttrType.java | 17 ++++ .../input/java/data/code/JavaCodeReader.java | 19 ++-- .../java/data/code/JavaInsnsRegister.java | 7 +- .../java/data/code/decoders/WideDecoder.java | 69 ++++++++++++++ .../api/plugins/input/data/IClassData.java | 3 +- .../api/plugins/input/data/ISeqConsumer.java | 13 +++ .../plugins/input/data/impl/ListConsumer.java | 35 +++++++ .../api/plugins/utils/CommonFileUtils.java | 56 +++++++++++ 21 files changed, 425 insertions(+), 111 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/others/TestJavaSwap.java create mode 100644 jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/WideDecoder.java create mode 100644 jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ISeqConsumer.java create mode 100644 jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/ListConsumer.java create mode 100644 jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/CommonFileUtils.java diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index 17741b200..5b83d40c4 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -6,10 +6,13 @@ dependencies { api(project(':jadx-plugins:jadx-plugins-api')) implementation 'com.google.code.gson:gson:2.8.7' - implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631' + implementation 'com.android.tools.build:aapt2-proto:4.2.1-7147631' testImplementation 'org.apache.commons:commons-lang3:3.12.0' + testImplementation 'org.ow2.asm:asm:9.2' + testImplementation 'org.ow2.asm:asm-util:9.2' + testRuntimeOnly(project(':jadx-plugins:jadx-dex-input')) testRuntimeOnly(project(':jadx-plugins:jadx-smali-input')) testRuntimeOnly(project(':jadx-plugins:jadx-java-convert')) diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 81278fdb5..cc8e631c2 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -586,7 +586,6 @@ public class InsnGen { case MOVE_MULTI: fallbackOnlyInsn(insn); - code.add("move-multi: "); int len = insn.getArgsCount(); for (int i = 0; i < len - 1; i += 2) { addArg(code, insn.getArg(i)); diff --git a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java index 6c37680e9..f88f5d12b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java @@ -147,6 +147,10 @@ public class AccessInfo { return (accFlags & AccessFlags.VOLATILE) != 0; } + public boolean isModuleInfo() { + return (accFlags & AccessFlags.MODULE) != 0; + } + public AFType getType() { return type; } @@ -200,6 +204,9 @@ public class AccessInfo { if ((accFlags & AccessFlags.STRICT) != 0) { code.append("strict "); } + if (isModuleInfo()) { + code.append("/* module-info */ "); + } if (Consts.DEBUG) { if ((accFlags & AccessFlags.SUPER) != 0) { code.append("/* super */ "); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index b42c2b5ec..2066c783b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -20,6 +20,8 @@ import jadx.api.ICodeCache; import jadx.api.ICodeInfo; import jadx.api.ICodeWriter; import jadx.api.plugins.input.data.IClassData; +import jadx.api.plugins.input.data.IFieldData; +import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultAttr; @@ -27,6 +29,7 @@ import jadx.api.plugins.input.data.attributes.types.AnnotationDefaultClassAttr; import jadx.api.plugins.input.data.attributes.types.InnerClassesAttr; import jadx.api.plugins.input.data.attributes.types.InnerClsInfo; import jadx.api.plugins.input.data.attributes.types.SourceFileAttr; +import jadx.api.plugins.input.data.impl.ListConsumer; import jadx.core.Consts; import jadx.core.ProcessClass; import jadx.core.dex.attributes.AFlag; @@ -97,34 +100,46 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN private void initialLoad(IClassData cls) { try { - String superType = cls.getSuperType(); - if (superType == null) { - // only java.lang.Object don't have super class - if (!clsInfo.getType().getObject().equals(Consts.CLASS_OBJECT)) { - throw new JadxRuntimeException("No super class in " + clsInfo.getType()); - } - this.superClass = null; - } else { - this.superClass = ArgType.object(superType); - } + addAttrs(cls.getAttributes()); + this.accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS); + this.superClass = checkSuperType(cls); this.interfaces = Utils.collectionMap(cls.getInterfacesTypes(), ArgType::object); - methods = new ArrayList<>(); - fields = new ArrayList<>(); - cls.visitFieldsAndMethods( - fld -> fields.add(FieldNode.build(this, fld)), - mth -> methods.add(MethodNode.build(this, mth))); + ListConsumer fieldsConsumer = new ListConsumer<>(fld -> FieldNode.build(this, fld)); + ListConsumer methodsConsumer = new ListConsumer<>(mth -> MethodNode.build(this, mth)); + cls.visitFieldsAndMethods(fieldsConsumer, methodsConsumer); + this.fields = fieldsConsumer.getResult(); + this.methods = methodsConsumer.getResult(); - addAttrs(cls.getAttributes()); - accessFlags = new AccessInfo(getAccessFlags(cls), AFType.CLASS); initStaticValues(fields); processAttributes(this); buildCache(); + + // TODO: implement module attribute parsing + if (this.accessFlags.isModuleInfo()) { + this.addWarnComment("Modules not supported yet"); + } } catch (Exception e) { throw new JadxRuntimeException("Error decode class: " + clsInfo, e); } } + private ArgType checkSuperType(IClassData cls) { + String superType = cls.getSuperType(); + if (superType == null) { + if (clsInfo.getType().getObject().equals(Consts.CLASS_OBJECT)) { + // java.lang.Object don't have super class + return null; + } + if (this.accessFlags.isModuleInfo()) { + // module-info also don't have super class + return null; + } + throw new JadxRuntimeException("No super class in " + clsInfo.getType()); + } + return ArgType.object(superType); + } + public void updateGenericClsData(ArgType superClass, List interfaces, List generics) { this.superClass = superClass; this.interfaces = interfaces; 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 bcfe013f8..3b8f0c3af 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 @@ -97,14 +97,7 @@ public class RootNode { } if (classes.size() != clsMap.size()) { // class name duplication detected - classes.stream().collect(Collectors.groupingBy(ClassNode::getClassInfo)) - .entrySet().stream() - .filter(entry -> entry.getValue().size() > 1) - .forEach(entry -> { - LOG.warn("Found duplicated class: {}, count: {}. Only one will be loaded!", entry.getKey(), - entry.getValue().size()); - entry.getValue().forEach(cls -> cls.addAttr(AType.COMMENTS, "WARNING: Classes with same name are omitted")); - }); + markDuplicatedClasses(classes); } classes = new ArrayList<>(clsMap.values()); // sort classes by name, expect top classes before inner @@ -135,6 +128,27 @@ public class RootNode { ErrorsCounter.error(clsNode, "Load error", exc); } + private static void markDuplicatedClasses(List classes) { + classes.stream() + .collect(Collectors.groupingBy(ClassNode::getClassInfo)) + .entrySet() + .stream() + .filter(entry -> entry.getValue().size() > 1) + .forEach(entry -> { + List sources = Utils.collectionMap(entry.getValue(), ClassNode::getInputFileName); + LOG.warn("Found duplicated class: {}, count: {}. Only one will be loaded!\n {}", + entry.getKey(), entry.getValue().size(), String.join("\n ", sources)); + entry.getValue().forEach(cls -> { + String thisSource = cls.getInputFileName(); + String otherSourceStr = sources.stream() + .filter(s -> !s.equals(thisSource)) + .sorted() + .collect(Collectors.joining("\n ")); + cls.addAttr(AType.COMMENTS, "WARNING: Classes with same name are omitted:\n " + otherSourceStr + '\n'); + }); + }); + } + public void addClassNode(ClassNode clsNode) { classes.add(clsNode); clsMap.put(clsNode.getClassInfo(), clsNode); diff --git a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java index 1d2bf2aeb..cd345359b 100644 --- a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java +++ b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java @@ -41,15 +41,15 @@ public abstract class BaseExternalTest extends IntegrationTest { return args; } - protected void decompile(JadxArgs jadxArgs) { - decompile(jadxArgs, null, null); + protected JadxDecompiler decompile(JadxArgs jadxArgs) { + return decompile(jadxArgs, null, null); } - protected void decompile(JadxArgs jadxArgs, String clsPatternStr) { - decompile(jadxArgs, clsPatternStr, null); + protected JadxDecompiler decompile(JadxArgs jadxArgs, String clsPatternStr) { + return decompile(jadxArgs, clsPatternStr, null); } - protected void decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) { + protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) { JadxDecompiler jadx = new JadxDecompiler(jadxArgs); jadx.getPluginManager().unload("java-convert"); jadx.load(); @@ -61,6 +61,7 @@ public abstract class BaseExternalTest extends IntegrationTest { processByPatterns(jadx, clsPatternStr, mthPatternStr); } printErrorReport(jadx); + return jadx; } private void processAll(JadxDecompiler jadx) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestJavaSwap.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestJavaSwap.java new file mode 100644 index 000000000..de439278a --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestJavaSwap.java @@ -0,0 +1,95 @@ +package jadx.tests.integration.others; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.util.CheckClassAdapter; + +import jadx.core.utils.files.FileUtils; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestJavaSwap extends IntegrationTest { + + @SuppressWarnings("StringBufferReplaceableByString") + public static class TestCls { + private Iterable field; + + @Override + public String toString() { + String string = String.valueOf(this.field); + return new StringBuilder(8 + String.valueOf(string).length()) + .append("concat(").append(string).append(")") + .toString(); + } + } + + @Test + public void testJava() { + useJavaInput(); + assertThat(getClassNode(TestCls.class)) + .code(); + } + + @Test + public void test() throws IOException { + // TODO: find up-to-date assembler/disassembler in java + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + cw.visit(Opcodes.V1_8, 0, "TestCls", null, "java/lang/Object", new String[] {}); + cw.visitField(Opcodes.ACC_PRIVATE, "field", "Ljava/lang/Iterable;", null, null).visitEnd(); + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, new String[] {}); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, "TestCls", "field", "Ljava/lang/Iterable;"); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false); + mv.visitVarInsn(Opcodes.ASTORE, 1); + mv.visitIntInsn(Opcodes.BIPUSH, 8); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "length", "()I", false); + mv.visitInsn(Opcodes.IADD); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP_X1); + mv.visitInsn(Opcodes.SWAP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(I)V", false); + mv.visitLdcInsn("concat("); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", + "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", + "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + mv.visitLdcInsn(")"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", + "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); // auto calculated + mv.visitEnd(); + cw.visitEnd(); + byte[] clsBytes = cw.toByteArray(); + + StringWriter results = new StringWriter(); + CheckClassAdapter.verify(new ClassReader(clsBytes), false, new PrintWriter(results)); + assertThat(results.toString()).isEmpty(); + + Path clsFile = FileUtils.createTempFile(".class"); + Files.write(clsFile, clsBytes); + List files = Collections.singletonList(clsFile.toFile()); + + useJavaInput(); + assertThat(getClassNodeFromFiles(files, "TestCls")) + .code(); + } +} diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java index 74b566b51..9e38e99d0 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodData; +import jadx.api.plugins.input.data.ISeqConsumer; import jadx.api.plugins.input.data.annotations.EncodedValue; import jadx.api.plugins.input.data.annotations.IAnnotation; import jadx.api.plugins.input.data.attributes.IJadxAttribute; @@ -92,7 +93,7 @@ public class DexClassData implements IClassData { } @Override - public void visitFieldsAndMethods(Consumer fieldConsumer, Consumer mthConsumer) { + public void visitFieldsAndMethods(ISeqConsumer fieldConsumer, ISeqConsumer mthConsumer) { int classDataOff = getClassDataOff(); if (classDataOff == 0) { return; @@ -103,6 +104,9 @@ public class DexClassData implements IClassData { int directMthCount = data.readUleb128(); int virtualMthCount = data.readUleb128(); + fieldConsumer.init(staticFieldsCount + instanceFieldsCount); + mthConsumer.init(directMthCount + virtualMthCount); + annotationsParser.setOffset(getAnnotationsOff()); visitFields(fieldConsumer, data, staticFieldsCount, instanceFieldsCount); visitMethods(mthConsumer, data, directMthCount, virtualMthCount); diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java index 6635e63a3..fcba420f7 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java @@ -1,9 +1,6 @@ package jadx.plugins.input.javaconvert; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -20,6 +17,7 @@ import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.plugins.utils.ZipSecurity; public class JavaConvertLoader { @@ -81,7 +79,7 @@ public class JavaConvertLoader { try { String entryName = entry.getName(); if (entryName.endsWith(".jar")) { - Path tempJar = saveInputStreamToFile(in, ".jar"); + Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar"); result.addTempPath(tempJar); LOG.debug("Loading jar: {} ...", entryName); convertJar(result, tempJar); @@ -134,14 +132,14 @@ public class JavaConvertLoader { || entryName.startsWith("META-INF/versions/")) { return; } - byte[] clsFileContent = inputStreamToByteArray(in); + byte[] clsFileContent = CommonFileUtils.loadBytes(in); String clsName = AsmUtils.getNameFromClassFile(clsFileContent); if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) { throw new IOException("Can't read class name from file: " + entryName); } addJarEntry(jo, clsName + ".class", clsFileContent, entry.getLastModifiedTime()); } else if (entryName.endsWith(".jar")) { - Path tempJar = saveInputStreamToFile(in, ".jar"); + Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar"); result.addTempPath(tempJar); convertJar(result, tempJar); } @@ -190,32 +188,4 @@ public class JavaConvertLoader { jar.write(content); jar.closeEntry(); } - - private static void copyStream(InputStream input, OutputStream output) throws IOException { - byte[] buffer = new byte[8 * 1024]; - while (true) { - int count = input.read(buffer); - if (count == -1) { - break; - } - output.write(buffer, 0, count); - } - } - - private static byte[] inputStreamToByteArray(InputStream input) throws IOException { - try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { - copyStream(input, output); - return output.toByteArray(); - } - } - - private static Path saveInputStreamToFile(InputStream in, String suffix) throws IOException { - Path tempJar = Files.createTempFile("jadx-temp-", suffix); - try (OutputStream out = Files.newOutputStream(tempJar)) { - copyStream(in, out); - } catch (Exception e) { - throw new IOException("Failed to save temp file", e); - } - return tempJar; - } } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java index 424d725ef..532b4d0fb 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaFileLoader.java @@ -1,7 +1,6 @@ package jadx.plugins.input.java; import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -13,9 +12,11 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.plugins.utils.CommonFileUtils; import jadx.api.plugins.utils.ZipSecurity; public class JavaFileLoader { @@ -38,35 +39,53 @@ public class JavaFileLoader { private List loadFromFile(File file) { try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) { - return loadReader(file, inputStream, file.getAbsolutePath()); + return loadReader(inputStream, file.getName(), file, null); } catch (Exception e) { LOG.error("File open error: {}", file.getAbsolutePath(), e); return Collections.emptyList(); } } - private List loadReader(File file, InputStream in, String inputFileName) throws IOException { + private List loadReader(InputStream in, String name, + @Nullable File file, @Nullable String parentFileName) throws IOException { byte[] magic = new byte[MAX_MAGIC_SIZE]; if (in.read(magic) != magic.length) { return Collections.emptyList(); } if (isStartWithBytes(magic, JAVA_CLASS_FILE_MAGIC)) { - byte[] data = loadBytes(magic, in); - JavaClassReader reader = new JavaClassReader(getNextUniqId(), inputFileName, data); + byte[] data = CommonFileUtils.loadBytes(magic, in); + String source = concatSource(parentFileName, name); + JavaClassReader reader = new JavaClassReader(getNextUniqId(), source, data); return Collections.singletonList(reader); } - if (file != null && isStartWithBytes(magic, ZIP_FILE_MAGIC)) { - return collectFromZip(file); + if (isStartWithBytes(magic, ZIP_FILE_MAGIC)) { + if (file != null) { + return collectFromZip(file, name); + } + File zipFile = CommonFileUtils.saveToTempFile(magic, in, ".zip").toFile(); + return collectFromZip(zipFile, concatSource(parentFileName, name)); } return Collections.emptyList(); } - private List collectFromZip(File file) { + private static String concatSource(@Nullable String parentFileName, String name) { + if (parentFileName == null) { + return name; + } + return parentFileName + ':' + name; + } + + private List collectFromZip(File file, String name) { List result = new ArrayList<>(); try { ZipSecurity.readZipEntries(file, (entry, in) -> { try { - result.addAll(loadReader(null, in, entry.getName())); + String entryName = entry.getName(); + if (entryName.startsWith("META-INF/versions/")) { + // skip classes for different java versions + return; + } + result.addAll(loadReader(in, entryName, null, name)); } catch (Exception e) { LOG.error("Failed to read zip entry: {}", entry, e); } @@ -90,21 +109,6 @@ public class JavaFileLoader { return true; } - public static byte[] loadBytes(byte[] prefix, InputStream in) throws IOException { - int estimateSize = prefix.length + in.available(); - ByteArrayOutputStream out = new ByteArrayOutputStream(estimateSize); - out.write(prefix); - byte[] buffer = new byte[8 * 1024]; - while (true) { - int len = in.read(buffer); - if (len == -1) { - break; - } - out.write(buffer, 0, len); - } - return out.toByteArray(); - } - private int getNextUniqId() { return classUniqId++; } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstPoolReader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstPoolReader.java index 07d531d36..232ea9635 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstPoolReader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/ConstPoolReader.java @@ -239,14 +239,17 @@ public class ConstPoolReader { private String fixType(String clsName) { switch (clsName.charAt(0)) { - case 'L': - case 'T': case '[': return clsName; - default: - return 'L' + clsName + ';'; + case 'L': + case 'T': + if (clsName.endsWith(";")) { + return clsName; + } + break; } + return 'L' + clsName + ';'; } private void jumpToData(int idx) { diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java index a7f1b5df3..6b21e2ee1 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java @@ -3,7 +3,6 @@ package jadx.plugins.input.java.data; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; @@ -11,6 +10,7 @@ import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.IMethodData; +import jadx.api.plugins.input.data.ISeqConsumer; import jadx.api.plugins.input.data.attributes.IJadxAttribute; import jadx.api.plugins.utils.Utils; import jadx.plugins.input.java.JavaClassReader; @@ -74,11 +74,12 @@ public class JavaClassData implements IClassData { } @Override - public void visitFieldsAndMethods(Consumer fieldsConsumer, Consumer mthConsumer) { + public void visitFieldsAndMethods(ISeqConsumer fieldsConsumer, ISeqConsumer mthConsumer) { int clsIdx = data.absPos(offsets.getClsTypeOffset()).readU2(); String classType = constPoolReader.getClass(clsIdx); DataReader reader = data.absPos(offsets.getFieldsOffset()).copy(); int fieldsCount = reader.readU2(); + fieldsConsumer.init(fieldsCount); if (fieldsCount != 0) { JavaFieldData field = new JavaFieldData(); field.setParentClassType(classType); @@ -89,6 +90,7 @@ public class JavaClassData implements IClassData { } int methodsCount = reader.readU2(); + mthConsumer.init(methodsCount); if (methodsCount != 0) { JavaMethodRef methodRef = new JavaMethodRef(); methodRef.setParentClassType(classType); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/AttributesReader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/AttributesReader.java index 22ae0128b..29dd77569 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/AttributesReader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/AttributesReader.java @@ -16,7 +16,7 @@ public class AttributesReader { private final JavaClassData clsData; private final ConstPoolReader constPool; - private final Map> attrMap = new HashMap<>(JavaAttrType.size()); + private final Map> attrCache = new HashMap<>(JavaAttrType.size()); public AttributesReader(JavaClassData clsData, ConstPoolReader constPoolReader) { this.clsData = clsData; @@ -95,8 +95,8 @@ public class AttributesReader { } private JavaAttrType resolveAttrReader(int nameIdx) { - return attrMap.computeIfAbsent(nameIdx, idx -> { - String attrName = constPool.getUtf8(nameIdx); + return attrCache.computeIfAbsent(nameIdx, idx -> { + String attrName = constPool.getUtf8(idx); JavaAttrType attrType = JavaAttrType.byName(attrName); if (attrType == null) { LOG.warn("Unknown java class attribute type: {}", attrName); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrType.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrType.java index 69e518d96..7164b15dc 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrType.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/attributes/JavaAttrType.java @@ -39,6 +39,8 @@ public final class JavaAttrType { public static final JavaAttrType BUILD_ANNOTATIONS; public static final JavaAttrType RUNTIME_PARAMETER_ANNOTATIONS; public static final JavaAttrType BUILD_PARAMETER_ANNOTATIONS; + public static final JavaAttrType RUNTIME_TYPE_ANNOTATIONS; + public static final JavaAttrType BUILD_TYPE_ANNOTATIONS; public static final JavaAttrType ANNOTATION_DEFAULT; public static final JavaAttrType SOURCE_FILE; @@ -46,7 +48,10 @@ public final class JavaAttrType { public static final JavaAttrType EXCEPTIONS; public static final JavaAttrType DEPRECATED; + public static final JavaAttrType SYNTHETIC; public static final JavaAttrType STACK_MAP_TABLE; + public static final JavaAttrType ENCLOSING_METHOD; + public static final JavaAttrType MODULE; static { NAME_TO_TYPE_MAP = new HashMap<>(); @@ -74,7 +79,14 @@ public final class JavaAttrType { // ignored DEPRECATED = bind("Deprecated", null); // duplicated by annotation + SYNTHETIC = bind("Synthetic", null); // duplicated by access flag STACK_MAP_TABLE = bind("StackMapTable", null); + ENCLOSING_METHOD = bind("EnclosingMethod", null); + + // TODO: not supported yet + RUNTIME_TYPE_ANNOTATIONS = bind("RuntimeVisibleTypeAnnotations", null); + BUILD_TYPE_ANNOTATIONS = bind("RuntimeInvisibleTypeAnnotations", null); + MODULE = bind("Module", null); } private static JavaAttrType bind(String name, IJavaAttributeReader reader) { @@ -113,4 +125,9 @@ public final class JavaAttrType { public IJavaAttributeReader getReader() { return reader; } + + @Override + public String toString() { + return name; + } } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaCodeReader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaCodeReader.java index fff1e81b0..dd2dee983 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaCodeReader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaCodeReader.java @@ -52,8 +52,7 @@ public class JavaCodeReader implements ICodeReader { @Override public void visitInstructions(Consumer insnConsumer) { Set excHandlers = getExcHandlers(); - reader.absPos(codeOffset); - int maxStack = reader.readU2(); + int maxStack = readMaxStack(); reader.skip(2); int codeSize = reader.readU4(); @@ -96,16 +95,20 @@ public class JavaCodeReader implements ICodeReader { @Override public int getRegistersCount() { - reader.absPos(codeOffset); - int maxStack = reader.readU2(); + int maxStack = readMaxStack(); int maxLocals = reader.readU2(); return maxStack + maxLocals; } @Override public int getArgsStartReg() { + return readMaxStack(); + } + + private int readMaxStack() { reader.absPos(codeOffset); - return reader.readU2(); // maxStack + int maxStack = reader.readU2(); + return maxStack + 1; // add one temporary register (for `swap` opcode) } @Override @@ -114,9 +117,9 @@ public class JavaCodeReader implements ICodeReader { } @Override - public @Nullable IDebugInfo getDebugInfo() { - reader.absPos(codeOffset); - int maxStack = reader.readU2(); + @Nullable + public IDebugInfo getDebugInfo() { + int maxStack = readMaxStack(); reader.skip(2); reader.skip(reader.readU4()); reader.skip(reader.readU2() * 8); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java index b6712c84c..1f53210e0 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/JavaInsnsRegister.java @@ -10,6 +10,7 @@ import jadx.plugins.input.java.data.code.decoders.InvokeDecoder; import jadx.plugins.input.java.data.code.decoders.LoadConstDecoder; import jadx.plugins.input.java.data.code.decoders.LookupSwitchDecoder; import jadx.plugins.input.java.data.code.decoders.TableSwitchDecoder; +import jadx.plugins.input.java.data.code.decoders.WideDecoder; import static jadx.plugins.input.java.data.code.StackState.SVType.NARROW; import static jadx.plugins.input.java.data.code.StackState.SVType.WIDE; @@ -172,6 +173,10 @@ public class JavaInsnsRegister { .peekFrom(2, 4).peekFrom(0, 5); } }); + register(arr, 0x5f, "swap", 0, 6, Opcode.MOVE_MULTI, + s -> s.peekFrom(-1, 0).peekFrom(1, 1) + .peekFrom(1, 2).peekFrom(0, 3) + .peekFrom(0, 4).peekFrom(-1, 5)); register(arr, 0x60, "iadd", 0, 3, Opcode.ADD_INT, twoRegsWithResult(NARROW)); register(arr, 0x61, "ladd", 0, 3, Opcode.ADD_LONG, twoRegsWithResult(WIDE)); @@ -295,7 +300,7 @@ public class JavaInsnsRegister { register(arr, 0xc2, "monitorenter", 0, 1, Opcode.MONITOR_ENTER, s -> s.pop(0)); register(arr, 0xc3, "monitorexit", 0, 1, Opcode.MONITOR_EXIT, s -> s.pop(0)); - // register(arr, 0xc4, "wide", 0, 1, Opcode.NOP, s -> s.pop(0)); + register(arr, 0xc4, "wide", -1, -1, Opcode.NOP, new WideDecoder()); register(arr, 0xc5, "multianewarray", 3, -1, Opcode.NEW_ARRAY, InsnIndexType.TYPE_REF, newArrayMulti()); register(arr, 0xc6, "ifnull", 2, 1, Opcode.IF_EQZ, zeroCmp()); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/WideDecoder.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/WideDecoder.java new file mode 100644 index 000000000..f06780455 --- /dev/null +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/code/decoders/WideDecoder.java @@ -0,0 +1,69 @@ +package jadx.plugins.input.java.data.code.decoders; + +import jadx.api.plugins.input.insns.Opcode; +import jadx.plugins.input.java.data.DataReader; +import jadx.plugins.input.java.data.code.CodeDecodeState; +import jadx.plugins.input.java.data.code.JavaInsnData; +import jadx.plugins.input.java.utils.JavaClassParseException; + +public class WideDecoder implements IJavaInsnDecoder { + private static final int IINC = 0x84; + + @Override + public void decode(CodeDecodeState state) { + DataReader reader = state.reader(); + JavaInsnData insn = state.insn(); + int opcode = reader.readU1(); + if (opcode == IINC) { + int varNum = reader.readU2(); + int constValue = reader.readS2(); + state.local(0, varNum).local(1, varNum).lit(constValue); + insn.setPayloadSize(5); + insn.setRegsCount(2); + insn.setOpcode(Opcode.ADD_INT_LIT); + return; + } + int index = reader.readU2(); + switch (opcode) { + case 0x15: // iload, + case 0x17: // fload + case 0x19: // aload + state.local(1, index).push(0); + break; + + case 0x16: // lload + case 0x18: // dload + state.local(1, index).pushWide(0); + break; + + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3a: + // *store + state.pop(1).local(0, index); + break; + + default: + throw new JavaClassParseException("Unexpected opcode in 'wide': 0x" + Integer.toHexString(opcode)); + } + insn.setPayloadSize(3); + insn.setRegsCount(2); + insn.setOpcode(Opcode.MOVE); + } + + @Override + public void skip(CodeDecodeState state) { + DataReader reader = state.reader(); + JavaInsnData insn = state.insn(); + int opcode = reader.readU1(); + if (opcode == IINC) { + reader.skip(4); + insn.setPayloadSize(5); + } else { + reader.skip(2); + insn.setPayloadSize(3); + } + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java index ef308ba1c..228656da6 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java @@ -1,7 +1,6 @@ package jadx.api.plugins.input.data; import java.util.List; -import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; @@ -21,7 +20,7 @@ public interface IClassData { List getInterfacesTypes(); - void visitFieldsAndMethods(Consumer fieldsConsumer, Consumer mthConsumer); + void visitFieldsAndMethods(ISeqConsumer fieldsConsumer, ISeqConsumer mthConsumer); List getAttributes(); diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ISeqConsumer.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ISeqConsumer.java new file mode 100644 index 000000000..43def1ef3 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/ISeqConsumer.java @@ -0,0 +1,13 @@ +package jadx.api.plugins.input.data; + +import java.util.function.Consumer; + +/** + * "Sequence consumer" allows getting count of elements available + */ +public interface ISeqConsumer extends Consumer { + + default void init(int count) { + // no-op implementation + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/ListConsumer.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/ListConsumer.java new file mode 100644 index 000000000..2202fa480 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/ListConsumer.java @@ -0,0 +1,35 @@ +package jadx.api.plugins.input.data.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import jadx.api.plugins.input.data.ISeqConsumer; + +public class ListConsumer implements ISeqConsumer { + private final Function convert; + private List list; + + public ListConsumer(Function convert) { + this.convert = convert; + } + + @Override + public void init(int count) { + list = count == 0 ? Collections.emptyList() : new ArrayList<>(count); + } + + @Override + public void accept(T t) { + list.add(convert.apply(t)); + } + + public List getResult() { + if (list == null) { + // init not called + return Collections.emptyList(); + } + return list; + } +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/CommonFileUtils.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/CommonFileUtils.java new file mode 100644 index 000000000..887eb0fa9 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/CommonFileUtils.java @@ -0,0 +1,56 @@ +package jadx.api.plugins.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CommonFileUtils { + + public static Path saveToTempFile(InputStream in, String suffix) throws IOException { + return saveToTempFile(null, in, suffix); + } + + public static Path saveToTempFile(byte[] dataPrefix, InputStream in, String suffix) throws IOException { + Path tempFile = Files.createTempFile("jadx-temp-", suffix); + try (OutputStream out = Files.newOutputStream(tempFile)) { + if (dataPrefix != null) { + out.write(dataPrefix); + } + copyStream(in, out); + } catch (Exception e) { + throw new IOException("Failed to save temp file", e); + } + return tempFile; + } + + public static byte[] loadBytes(InputStream input) throws IOException { + return loadBytes(null, input); + } + + public static byte[] loadBytes(byte[] dataPrefix, InputStream in) throws IOException { + int estimateSize = dataPrefix == null ? in.available() : dataPrefix.length + in.available(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream(estimateSize)) { + if (dataPrefix != null) { + out.write(dataPrefix); + } + copyStream(in, out); + return out.toByteArray(); + } catch (Exception e) { + throw new IOException("Failed to read input stream to bytes array", e); + } + } + + public static void copyStream(InputStream input, OutputStream output) throws IOException { + byte[] buffer = new byte[8 * 1024]; + while (true) { + int count = input.read(buffer); + if (count == -1) { + break; + } + output.write(buffer, 0, count); + } + } +}