From c47e9cdde4291d9f9ad661d7e288f2ac99d9226a Mon Sep 17 00:00:00 2001 From: Skylot Date: Mon, 4 Jan 2021 20:04:28 +0300 Subject: [PATCH] fix: allow to load Spring Boot jar (#1066) --- .../main/java/jadx/api/ResourcesLoader.java | 5 +- .../plugins/input/javaconvert/AsmUtils.java | 26 ++++ .../input/javaconvert/JavaConvertLoader.java | 111 ++++++++++++++---- .../jadx/api/plugins/utils/ZipSecurity.java | 28 +++-- 4 files changed, 141 insertions(+), 29 deletions(-) create mode 100644 jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/AsmUtils.java diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 4e3d9248b..224705503 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -128,7 +128,10 @@ public final class ResourcesLoader { return; } if (FileUtils.isZipFile(file)) { - ZipSecurity.visitZipEntries(file, (zipFile, entry) -> addEntry(list, file, entry)); + ZipSecurity.visitZipEntries(file, (zipFile, entry) -> { + addEntry(list, file, entry); + return null; + }); } else { addResourceFile(list, file); } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/AsmUtils.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/AsmUtils.java new file mode 100644 index 000000000..6ae6eaa1f --- /dev/null +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/AsmUtils.java @@ -0,0 +1,26 @@ +package jadx.plugins.input.javaconvert; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.objectweb.asm.ClassReader; + +public class AsmUtils { + + public static String getNameFromClassFile(Path file) throws IOException { + try (InputStream in = Files.newInputStream(file)) { + return getClassFullName(new ClassReader(in)); + } + } + + public static String getNameFromClassFile(byte[] content) throws IOException { + return getClassFullName(new ClassReader(content)); + } + + private static String getClassFullName(ClassReader classReader) { + return classReader.getClassName(); + } + +} 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 65c07312f..124c6555c 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,6 +1,6 @@ package jadx.plugins.input.javaconvert; -import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -9,13 +9,14 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.nio.file.attribute.FileTime; import java.util.List; +import java.util.Objects; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.objectweb.asm.ClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +42,7 @@ public class JavaConvertLoader { try { convertJar(result, path); } catch (Exception e) { - LOG.error("Failed to convert file: " + path.toAbsolutePath(), e); + LOG.error("Failed to convert file: {}", path.toAbsolutePath(), e); } }); } @@ -59,7 +60,7 @@ public class JavaConvertLoader { Path jarFile = Files.createTempFile("jadx-", ".jar"); try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { for (Path file : clsFiles) { - String clsName = getNameFromClassFile(file); + String clsName = AsmUtils.getNameFromClassFile(file); if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) { throw new IOException("Can't read class name from file: " + file); } @@ -74,13 +75,6 @@ public class JavaConvertLoader { } } - public static String getNameFromClassFile(Path file) throws IOException { - try (InputStream in = Files.newInputStream(file)) { - ClassReader classReader = new ClassReader(in); - return classReader.getClassName(); - } - } - private static void processAars(List input, ConvertResult result) { PathMatcher aarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.aar"); input.stream() @@ -100,6 +94,68 @@ public class JavaConvertLoader { } private static void convertJar(ConvertResult result, Path path) throws Exception { + if (repackAndConvertJar(result, path)) { + return; + } + convertSimpleJar(result, path); + } + + private static boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception { + // check if jar need a full repackage + Boolean repackNeeded = ZipSecurity.visitZipEntries(path.toFile(), (zipFile, zipEntry) -> { + String entryName = zipEntry.getName(); + if (zipEntry.isDirectory()) { + if (entryName.equals("BOOT-INF/")) { + return true; // Spring Boot jar + } + if (entryName.equals("META-INF/versions/")) { + return true; // exclude duplicated classes + } + } + if (entryName.endsWith(".jar")) { + return true; // contains sub jars + } + if (entryName.endsWith("module-info.class")) { + return true; // need to exclude module files + } + return null; + }); + if (!Objects.equals(repackNeeded, Boolean.TRUE)) { + return false; + } + + Path jarFile = Files.createTempFile("jadx-classes-", ".jar"); + result.addTempPath(jarFile); + try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { + ZipSecurity.readZipEntries(path.toFile(), (entry, in) -> { + try { + String entryName = entry.getName(); + if (entryName.endsWith(".class")) { + if (entryName.endsWith("module-info.class") + || entryName.startsWith("META-INF/versions/")) { + return; + } + byte[] clsFileContent = inputStreamToByteArray(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"); + result.addTempPath(tempJar); + convertJar(result, tempJar); + } + } catch (Exception e) { + LOG.error("Failed to process jar entry: {} in {}", entry, path, e); + } + }); + } + convertSimpleJar(result, jarFile); + return true; + } + + private static void convertSimpleJar(ConvertResult result, Path path) throws Exception { Path tempDirectory = Files.createTempDirectory("jadx-"); result.addTempPath(tempDirectory); @@ -119,18 +175,24 @@ public class JavaConvertLoader { } } - public static void addFileToJar(JarOutputStream jar, Path source, String entryName) throws IOException { - try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(source))) { - JarEntry entry = new JarEntry(entryName); - entry.setTime(Files.getLastModifiedTime(source, LinkOption.NOFOLLOW_LINKS).toMillis()); - jar.putNextEntry(entry); - - copyStream(in, jar); - jar.closeEntry(); - } + private static void addFileToJar(JarOutputStream jar, Path source, String entryName) throws IOException { + byte[] fileContent = Files.readAllBytes(source); + FileTime lastModifiedTime = Files.getLastModifiedTime(source, LinkOption.NOFOLLOW_LINKS); + addJarEntry(jar, entryName, fileContent, lastModifiedTime); } - public static void copyStream(InputStream input, OutputStream output) throws IOException { + private static void addJarEntry(JarOutputStream jar, String entryName, byte[] content, + FileTime modTime) throws IOException { + JarEntry entry = new JarEntry(entryName); + if (modTime != null) { + entry.setTime(modTime.toMillis()); + } + jar.putNextEntry(entry); + 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); @@ -141,6 +203,13 @@ public class JavaConvertLoader { } } + 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)) { diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/ZipSecurity.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/ZipSecurity.java index 983b0bcfe..f6865c332 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/ZipSecurity.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/utils/ZipSecurity.java @@ -6,9 +6,11 @@ import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,14 +88,22 @@ public class ZipSecurity { return new BufferedInputStream(limited); } - public static void visitZipEntries(File file, BiConsumer visitor) { + /** + * Visit valid entries in zip file. + * Return not null value from visitor to stop iteration. + */ + @Nullable + public static R visitZipEntries(File file, BiFunction visitor) { try (ZipFile zip = new ZipFile(file)) { Enumeration entries = zip.entries(); int entriesProcessed = 0; while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); - if (!entry.isDirectory() && isValidZipEntry(entry)) { - visitor.accept(zip, entry); + if (isValidZipEntry(entry)) { + R result = visitor.apply(zip, entry); + if (result != null) { + return result; + } entriesProcessed++; if (entriesProcessed > MAX_ENTRIES_COUNT) { throw new IllegalStateException("Zip entries count limit exceeded: " + MAX_ENTRIES_COUNT @@ -104,15 +114,19 @@ public class ZipSecurity { } catch (Exception e) { throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e); } + return null; } public static void readZipEntries(File file, BiConsumer visitor) { visitZipEntries(file, (zip, entry) -> { - try (InputStream in = getInputStreamForEntry(zip, entry)) { - visitor.accept(entry, in); - } catch (Exception e) { - throw new RuntimeException("Error process zip entry: " + entry.getName()); + if (!entry.isDirectory()) { + try (InputStream in = getInputStreamForEntry(zip, entry)) { + visitor.accept(entry, in); + } catch (Exception e) { + throw new RuntimeException("Error process zip entry: " + entry.getName()); + } } + return null; }); } }