|
| 1 | +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |
| 2 | +From: zml <zml@stellardrift.ca> |
| 3 | +Date: Sat, 2 Apr 2022 23:57:57 -0700 |
| 4 | +Subject: [PATCH] Add jrt option to load classes from a specific JVM |
| 5 | + |
| 6 | +This allows decompiling classes targeting any Java version while running on a Java 17 JVM |
| 7 | + |
| 8 | +diff --git a/README.md b/README.md |
| 9 | +index 225b42bcbd388c395fe51d2c5edda8d1cb1127b2..65a07c7d2ca1e906f0bc5d7ea6bd221515c582cd 100644 |
| 10 | +--- a/README.md |
| 11 | ++++ b/README.md |
| 12 | +@@ -66,6 +66,7 @@ The rest of options can be left as they are: they are aimed at professional reve |
| 13 | + - nls (0): define new line character to be used for output. 0 - '\r\n' (Windows), 1 - '\n' (Unix), default is OS-dependent |
| 14 | + - ind: indentation string (default is 3 spaces) |
| 15 | + - log (INFO): a logging level, possible values are TRACE, INFO, WARN, ERROR |
| 16 | ++- jrt (): The path to a java runtime to add to the classpath, or `1` or `current` to add the java runtime of the active JVM to the classpath. |
| 17 | + |
| 18 | + ### Renaming identifiers |
| 19 | + |
| 20 | +diff --git a/src/org/jetbrains/java/decompiler/main/Fernflower.java b/src/org/jetbrains/java/decompiler/main/Fernflower.java |
| 21 | +index a9cb134d0e7907472b1abde8dd4fcbe4383dc4cc..a7a1a48ac004c25e5d578a2624c1f477a9d9936c 100644 |
| 22 | +--- a/src/org/jetbrains/java/decompiler/main/Fernflower.java |
| 23 | ++++ b/src/org/jetbrains/java/decompiler/main/Fernflower.java |
| 24 | +@@ -93,7 +93,14 @@ public class Fernflower implements IDecompiledData { |
| 25 | + logger.writeMessage(String.format("JVM info: %s - %s - %s", vendor, javaVersion, jvmVersion), IFernflowerLogger.Severity.INFO); |
| 26 | + |
| 27 | + if (DecompilerContext.getOption(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH)) { |
| 28 | +- ClasspathScanner.addAllClasspath(structContext); |
| 29 | ++ ClasspathScanner.addAllClasspath(structContext, false); |
| 30 | ++ } else if (!DecompilerContext.getProperty(IFernflowerPreferences.INCLUDE_JAVA_RUNTIME).toString().isEmpty()) { |
| 31 | ++ final String javaRuntime = DecompilerContext.getProperty(IFernflowerPreferences.INCLUDE_JAVA_RUNTIME).toString(); |
| 32 | ++ if (javaRuntime.equalsIgnoreCase("current") || javaRuntime.equalsIgnoreCase("1")) { |
| 33 | ++ ClasspathScanner.addAllClasspath(structContext, true); |
| 34 | ++ } else { |
| 35 | ++ ClasspathScanner.addRuntime(structContext, new File(javaRuntime)); |
| 36 | ++ } |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | +diff --git a/src/org/jetbrains/java/decompiler/main/extern/IFernflowerPreferences.java b/src/org/jetbrains/java/decompiler/main/extern/IFernflowerPreferences.java |
| 41 | +index 23ed017fc66747daf2f7b603df7d123835145570..a72ad0aba0523cd803b4832b9e95d7f67c8d1d7c 100644 |
| 42 | +--- a/src/org/jetbrains/java/decompiler/main/extern/IFernflowerPreferences.java |
| 43 | ++++ b/src/org/jetbrains/java/decompiler/main/extern/IFernflowerPreferences.java |
| 44 | +@@ -35,6 +35,7 @@ public interface IFernflowerPreferences { |
| 45 | + String IGNORE_INVALID_BYTECODE = "iib"; |
| 46 | + String VERIFY_ANONYMOUS_CLASSES = "vac"; |
| 47 | + |
| 48 | ++ String INCLUDE_JAVA_RUNTIME = "jrt"; |
| 49 | + String INCLUDE_ENTIRE_CLASSPATH = "iec"; |
| 50 | + String EXPLICIT_GENERIC_ARGUMENTS = "ega"; |
| 51 | + String INLINE_SIMPLE_LAMBDAS = "isl"; |
| 52 | +@@ -92,6 +93,7 @@ public interface IFernflowerPreferences { |
| 53 | + defaults.put(IGNORE_INVALID_BYTECODE, "0"); |
| 54 | + defaults.put(VERIFY_ANONYMOUS_CLASSES, "0"); |
| 55 | + |
| 56 | ++ defaults.put(INCLUDE_JAVA_RUNTIME, ""); |
| 57 | + defaults.put(INCLUDE_ENTIRE_CLASSPATH, "0"); |
| 58 | + defaults.put(EXPLICIT_GENERIC_ARGUMENTS, "0"); |
| 59 | + defaults.put(INLINE_SIMPLE_LAMBDAS, "1"); |
| 60 | +diff --git a/src/org/jetbrains/java/decompiler/util/ClasspathScanner.java b/src/org/jetbrains/java/decompiler/util/ClasspathScanner.java |
| 61 | +index 0cdeb5bbd8581965a96211dabc17bc18f3cde687..c0d96578d5c74abfd8deb3537e9f4a6799099a0b 100644 |
| 62 | +--- a/src/org/jetbrains/java/decompiler/util/ClasspathScanner.java |
| 63 | ++++ b/src/org/jetbrains/java/decompiler/util/ClasspathScanner.java |
| 64 | +@@ -2,13 +2,23 @@ |
| 65 | + package org.jetbrains.java.decompiler.util; |
| 66 | + |
| 67 | + import java.lang.module.*; |
| 68 | ++import java.net.URI; |
| 69 | ++import java.nio.file.FileSystem; |
| 70 | ++import java.nio.file.FileSystems; |
| 71 | ++import java.nio.file.Files; |
| 72 | ++import java.nio.file.Path; |
| 73 | + import java.io.File; |
| 74 | + import java.io.IOException; |
| 75 | + import java.io.InputStream; |
| 76 | ++import java.util.ArrayDeque; |
| 77 | + import java.util.ArrayList; |
| 78 | ++import java.util.Collections; |
| 79 | ++import java.util.Deque; |
| 80 | + import java.util.HashSet; |
| 81 | + import java.util.List; |
| 82 | ++import java.util.Map; |
| 83 | + import java.util.Set; |
| 84 | ++import java.util.stream.Stream; |
| 85 | + |
| 86 | + import org.jetbrains.java.decompiler.main.DecompilerContext; |
| 87 | + import org.jetbrains.java.decompiler.main.extern.IContextSource; |
| 88 | +@@ -18,9 +28,9 @@ import org.jetbrains.java.decompiler.struct.StructContext; |
| 89 | + |
| 90 | + public class ClasspathScanner { |
| 91 | + |
| 92 | +- public static void addAllClasspath(StructContext ctx) { |
| 93 | +- Set<String> found = new HashSet<String>(); |
| 94 | +- String[] props = { System.getProperty("java.class.path"), System.getProperty("sun.boot.class.path") }; |
| 95 | ++ public static void addAllClasspath(StructContext ctx, boolean bootOnly) { |
| 96 | ++ Set<String> found = new HashSet<>(); |
| 97 | ++ String[] props = { bootOnly ? null : System.getProperty("java.class.path"), System.getProperty("sun.boot.class.path") }; |
| 98 | + for (String prop : props) { |
| 99 | + if (prop == null) |
| 100 | + continue; |
| 101 | +@@ -38,34 +48,93 @@ public class ClasspathScanner { |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | +- addAllModulePath(ctx); |
| 106 | ++ addAllModulePath(ctx, bootOnly); |
| 107 | + } |
| 108 | + |
| 109 | +- private static void addAllModulePath(StructContext ctx) { |
| 110 | +- for (ModuleReference module : ModuleFinder.ofSystem().findAll()) { |
| 111 | +- String name = module.descriptor().name(); |
| 112 | ++ // https://openjdk.java.net/jeps/220 for runtime image structure and JRT filesystem |
| 113 | ++ |
| 114 | ++ private static void addAllModulePath(StructContext ctx, boolean bootOnly) { |
| 115 | ++ final var finder = ModuleFinder.ofSystem(); |
| 116 | ++ if (bootOnly) { |
| 117 | ++ final ModuleReference ref = finder.find("java.se").orElseThrow(() -> new IllegalStateException("Could not find java.se module required by spec")); |
| 118 | ++ final Set<String> seen = new HashSet<>(); |
| 119 | ++ final Deque<ModuleReference> toVisit = new ArrayDeque<>(); |
| 120 | ++ toVisit.add(ref); |
| 121 | ++ ModuleReference current; |
| 122 | ++ while ((current = toVisit.poll()) != null) { |
| 123 | ++ if (!seen.add(current.descriptor().name())) { |
| 124 | ++ continue; |
| 125 | ++ } |
| 126 | ++ |
| 127 | ++ for (var requires : ref.descriptor().requires()) { |
| 128 | ++ var module = finder.find(requires.name()).orElse(null); |
| 129 | ++ if (module == null && !requires.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) { |
| 130 | ++ DecompilerContext.getLogger().writeMessage("Could not find Java SE component " + requires.name(), IFernflowerLogger.Severity.ERROR); |
| 131 | ++ continue; |
| 132 | ++ } |
| 133 | ++ toVisit.add(module); |
| 134 | ++ } |
| 135 | ++ |
| 136 | ++ try { |
| 137 | ++ ctx.addSpace(new ModuleContextSource(current), false); |
| 138 | ++ } catch (IOException e) { |
| 139 | ++ DecompilerContext.getLogger().writeMessage("Error loading module " + current.descriptor().name(), e); |
| 140 | ++ } |
| 141 | ++ } |
| 142 | ++ } else { |
| 143 | ++ for (ModuleReference module : finder.findAll()) { |
| 144 | ++ String name = module.descriptor().name(); |
| 145 | ++ try { |
| 146 | ++ ctx.addSpace(new ModuleContextSource(module), false); |
| 147 | ++ } catch (IOException e) { |
| 148 | ++ DecompilerContext.getLogger().writeMessage("Error loading module " + name, e); |
| 149 | ++ } |
| 150 | ++ } |
| 151 | ++ } |
| 152 | ++ } |
| 153 | ++ |
| 154 | ++ public static void addRuntime(final StructContext ctx, final File javaHome) { |
| 155 | ++ if (new File(javaHome, "lib/jrt-fs.jar").isFile()) { |
| 156 | ++ // Java 9+ |
| 157 | + try { |
| 158 | +- ctx.addSpace(new ModuleContextSource(module), false); |
| 159 | +- } catch (IOException e) { |
| 160 | +- DecompilerContext.getLogger().writeMessage("Error loading module " + name, e); |
| 161 | ++ ctx.addSpace(new JavaRuntimeContextSource(javaHome), false); |
| 162 | ++ } catch (final IOException ex) { |
| 163 | ++ DecompilerContext.getLogger().writeMessage("Failed to open java runtime at " + javaHome, ex); |
| 164 | + } |
| 165 | ++ return; |
| 166 | ++ } else if (javaHome.exists()) { |
| 167 | ++ // legacy runtime, add all jars from the lib and jre/lib folders |
| 168 | ++ boolean anyAdded = false; |
| 169 | ++ final List<File> jrt = new ArrayList<>(); |
| 170 | ++ Collections.addAll(jrt, new File(javaHome, "jre/lib").listFiles()); |
| 171 | ++ Collections.addAll(jrt, new File(javaHome, "lib").listFiles()); |
| 172 | ++ for (final File lib : jrt) { |
| 173 | ++ if (lib.isFile() && lib.getName().endsWith(".jar")) { |
| 174 | ++ ctx.addSpace(lib, false); |
| 175 | ++ anyAdded = true; |
| 176 | ++ } |
| 177 | ++ } |
| 178 | ++ if (anyAdded) return; |
| 179 | + } |
| 180 | ++ |
| 181 | ++ // does not exist |
| 182 | ++ DecompilerContext.getLogger().writeMessage("Unable to detect a java runtime at " + javaHome, IFernflowerLogger.Severity.WARN); |
| 183 | + } |
| 184 | + |
| 185 | +- static class ModuleContextSource implements IContextSource, AutoCloseable { |
| 186 | +- private final ModuleReference ref; |
| 187 | +- private final ModuleReader reader; |
| 188 | ++ static abstract class ModuleBasedContextSource implements IContextSource { |
| 189 | ++ private final ModuleDescriptor ref; |
| 190 | + |
| 191 | +- public ModuleContextSource(final ModuleReference ref) throws IOException { |
| 192 | ++ public ModuleBasedContextSource(final ModuleDescriptor ref) { |
| 193 | + this.ref = ref; |
| 194 | +- this.reader = ref.open(); |
| 195 | + } |
| 196 | + |
| 197 | + @Override |
| 198 | + public String getName() { |
| 199 | +- return "module " + this.ref.descriptor().toNameAndVersion(); |
| 200 | ++ return "module " + this.ref.toNameAndVersion(); |
| 201 | + } |
| 202 | + |
| 203 | ++ protected abstract Stream<String> entryNames() throws IOException; |
| 204 | ++ |
| 205 | + @Override |
| 206 | + public Entries getEntries() { |
| 207 | + final List<String> classNames = new ArrayList<>(); |
| 208 | +@@ -73,7 +142,7 @@ public class ClasspathScanner { |
| 209 | + final List<String> otherEntries = new ArrayList<>(); |
| 210 | + |
| 211 | + try { |
| 212 | +- this.reader.list().forEach(name -> { |
| 213 | ++ this.entryNames().forEach(name -> { |
| 214 | + if (name.endsWith("/")) { |
| 215 | + directoryNames.add(name.substring(0, name.length() - 1)); |
| 216 | + } else if (name.endsWith(CLASS_SUFFIX)) { |
| 217 | +@@ -88,6 +157,20 @@ public class ClasspathScanner { |
| 218 | + |
| 219 | + return new Entries(classNames, directoryNames, otherEntries); |
| 220 | + } |
| 221 | ++ } |
| 222 | ++ |
| 223 | ++ static class ModuleContextSource extends ModuleBasedContextSource implements AutoCloseable { |
| 224 | ++ private final ModuleReader reader; |
| 225 | ++ |
| 226 | ++ public ModuleContextSource(final ModuleReference ref) throws IOException { |
| 227 | ++ super(ref.descriptor()); |
| 228 | ++ this.reader = ref.open(); |
| 229 | ++ } |
| 230 | ++ |
| 231 | ++ @Override |
| 232 | ++ public Stream<String> entryNames() throws IOException { |
| 233 | ++ return this.reader.list(); |
| 234 | ++ } |
| 235 | + |
| 236 | + @Override |
| 237 | + public InputStream getInputStream(String resource) throws IOException { |
| 238 | +@@ -99,4 +182,74 @@ public class ClasspathScanner { |
| 239 | + this.reader.close(); |
| 240 | + } |
| 241 | + } |
| 242 | ++ |
| 243 | ++ static final class JavaRuntimeModuleContextSource extends ModuleBasedContextSource { |
| 244 | ++ private Path module; |
| 245 | ++ |
| 246 | ++ JavaRuntimeModuleContextSource(final ModuleDescriptor descriptor, final Path moduleRoot) { |
| 247 | ++ super(descriptor); |
| 248 | ++ this.module = moduleRoot; |
| 249 | ++ } |
| 250 | ++ |
| 251 | ++ @Override |
| 252 | ++ public InputStream getInputStream(String resource) throws IOException { |
| 253 | ++ return Files.newInputStream(this.module.resolve(resource)); |
| 254 | ++ } |
| 255 | ++ |
| 256 | ++ @Override |
| 257 | ++ protected Stream<String> entryNames() throws IOException { |
| 258 | ++ try (final var dir = Files.walk(this.module)) { |
| 259 | ++ return dir.map(it -> this.module.relativize(it).toString()).toList().stream(); |
| 260 | ++ } |
| 261 | ++ } |
| 262 | ++ } |
| 263 | ++ |
| 264 | ++ static final class JavaRuntimeContextSource implements IContextSource, AutoCloseable { |
| 265 | ++ private final File javaHome; |
| 266 | ++ private final FileSystem jrtFileSystem; |
| 267 | ++ |
| 268 | ++ public JavaRuntimeContextSource(final File javaHome) throws IOException { |
| 269 | ++ this.javaHome = javaHome; |
| 270 | ++ final var url = URI.create("jrt:/"); |
| 271 | ++ this.jrtFileSystem = FileSystems.newFileSystem(url, Map.of("java.home", javaHome.getAbsolutePath())); |
| 272 | ++ } |
| 273 | ++ |
| 274 | ++ @Override |
| 275 | ++ public String getName() { |
| 276 | ++ return "Java runtime " + this.javaHome.getAbsolutePath(); |
| 277 | ++ } |
| 278 | ++ |
| 279 | ++ @Override |
| 280 | ++ public Entries getEntries() { |
| 281 | ++ // One child source for every module in the runtime |
| 282 | ++ final List<IContextSource> children = new ArrayList<>(); |
| 283 | ++ try { |
| 284 | ++ final List<Path> modules = Files.list(this.jrtFileSystem.getPath("modules")).toList(); |
| 285 | ++ for (final Path module : modules) { |
| 286 | ++ ModuleDescriptor descriptor; |
| 287 | ++ try (final InputStream is = Files.newInputStream(module.resolve("module-info.class"))) { |
| 288 | ++ descriptor = ModuleDescriptor.read(is); |
| 289 | ++ } catch (final IOException ex) { |
| 290 | ++ continue; |
| 291 | ++ } |
| 292 | ++ children.add(new JavaRuntimeModuleContextSource(descriptor, module)); |
| 293 | ++ } |
| 294 | ++ |
| 295 | ++ return new Entries(List.of(), List.of(), List.of(), children); |
| 296 | ++ } catch (final IOException ex) { |
| 297 | ++ DecompilerContext.getLogger().writeMessage("Failed to read modules from runtime " + this.javaHome, ex); |
| 298 | ++ return Entries.EMPTY; |
| 299 | ++ } |
| 300 | ++ } |
| 301 | ++ |
| 302 | ++ @Override |
| 303 | ++ public InputStream getInputStream(String resource) throws IOException { |
| 304 | ++ return null; // all resources are part of a child provider |
| 305 | ++ } |
| 306 | ++ |
| 307 | ++ @Override |
| 308 | ++ public void close() throws IOException { |
| 309 | ++ this.jrtFileSystem.close(); |
| 310 | ++ } |
| 311 | ++ } |
| 312 | + } |
0 commit comments