Skip to content

Commit 84102c7

Browse files
committed
Add -jrt option to enable decompiling with classes from another JVM
1 parent 98a4ff8 commit 84102c7

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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

Comments
 (0)