diff --git a/README.md b/README.md
index 6bc9baf1..a21eb45a 100644
--- a/README.md
+++ b/README.md
@@ -6,20 +6,39 @@ A deobfuscator for java
## ✅ How to run deobfuscator
If you want to use this deobfuscator, you need to start it from your IDE manually.
+### Prerequisites
+**Important:** You need TWO different Java installations:
+- **[Java 17](https://adoptium.net/temurin/releases/?version=17)** - Required for the project to compile and run
+- **[Java 8](https://adoptium.net/temurin/releases/?version=8)** - Required for the sandbox (SSVM) to work properly
+
+### Instructions
1. Clone this repository and open it in IntelliJ
-2. Make sure that you have selected [Java 17 (Temurin)](https://adoptium.net/temurin/releases/?version=17) in `Project Structure` -> `SDK`
-3. Place your obfuscated jar inside the root project directory. For example in `work/obf-test.jar`
-4. Navigate to class [`Bootstrap.java`](./deobfuscator-impl/src/test/java/Bootstrap.java)
-5. In this class edit the deobfuscator configuration
- - `inputJar` - Your obfuscated jar file that you placed in step 1
+2. Make sure that you have selected [Java 17](https://adoptium.net/temurin/releases/?version=17) in `Project Structure` -> `SDK`
+3. Install [Java 8](https://adoptium.net/temurin/releases/?version=8) if you don't have it already
+4. Place your obfuscated jar inside the root project directory. For example in `work/obf-test.jar`
+5. Navigate to class [`Bootstrap.java`](./deobfuscator-impl/src/test/java/Bootstrap.java)
+6. In this class edit the deobfuscator configuration
+ - `inputJar` - Your obfuscated jar file that you placed in step 4
- `transformers` - Pick transformers that you want to run. You can find them in [`deobfuscator-transformers`](./deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other) module.
-6. Run this class manually from your IDE. You can use our pre-configured IntelliJ task named `Bootstrap`.
+7. Run this class manually from your IDE. You can use our pre-configured IntelliJ task named `Bootstrap`.

## 🔧 Contributing
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for a project introduction and some basics about java bytecode.
+## ❓ FAQ
+
+**Q: Sandbox doesn't work / "rt.jar is required for sandbox to run" error**
+
+A: The sandbox requires rt.jar from **[Java 8](https://adoptium.net/temurin/releases/?version=8)** installation. The deobfuscator will try to auto-detect it, but if it fails:
+- Make sure you have [Java 8](https://adoptium.net/temurin/releases/?version=8) installed
+- You can manually set it via system property: `-DrtJarPath="path/to/rt.jar"`
+- Or specify it in your Bootstrap configuration: `.rtJarPath(Path.of("path/to/rt.jar"))`
+- Common rt.jar locations (may vary based on installation):
+ - Oracle JDK 8: `C:/Program Files/Java/jdk1.8.0_202/jre/lib/rt.jar`
+ - Eclipse Adoptium JDK 8: `C:/Program Files/Eclipse Adoptium/jdk-8.0.462.8-hotspot/jre/lib/rt.jar`
+
## Links
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java
index 8ac7e312..3a7ec3e7 100644
--- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java
@@ -1,9 +1,14 @@
package uwu.narumi.deobfuscator.api.context;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.ClassWriter;
+import uwu.narumi.deobfuscator.api.environment.JavaEnv;
+import uwu.narumi.deobfuscator.api.environment.JavaInstall;
+import uwu.narumi.deobfuscator.api.execution.SandBox;
import uwu.narumi.deobfuscator.api.transformer.Transformer;
import java.io.IOException;
@@ -15,6 +20,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
@@ -25,6 +31,7 @@ public record DeobfuscatorOptions(
@Nullable Path inputJar,
List externalFiles,
Set libraries,
+ @Nullable Path rtJarPath,
@Nullable Path outputJar,
@Nullable Path outputDir,
@@ -53,11 +60,15 @@ public record ExternalFile(Path path, String pathInJar) {
* Builder for {@link DeobfuscatorOptions}
*/
public static class Builder {
+ private static final Logger LOGGER = LogManager.getLogger();
+
// Inputs
@Nullable
private Path inputJar = null;
private final List externalFiles = new ArrayList<>();
private final Set libraries = new HashSet<>();
+ @Nullable
+ private Path rtJarPath = null;
// Outputs
@Nullable
@@ -161,6 +172,18 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
return this;
}
+ /**
+ * Path to rt.jar from Java 8 binaries. Required for sandbox to work properly.
+ * Examples:
+ * - Oracle JDK 8: C:/Program Files/Java/jdk1.8.0_202/jre/lib/rt.jar
+ * - Eclipse Adoptium JDK 8: C:/Program Files/Eclipse Adoptium/jdk-8.0.462.8-hotspot/jre/lib/rt.jar
+ */
+ @Contract("_ -> this")
+ public DeobfuscatorOptions.Builder rtJarPath(@Nullable Path rtJarPath) {
+ this.rtJarPath = rtJarPath;
+ return this;
+ }
+
/**
* Output jar for deobfuscated classes. Automatically filled when input jar is set
*/
@@ -255,6 +278,30 @@ public DeobfuscatorOptions.Builder skipFiles() {
return this;
}
+ /**
+ * Try to find rt.jar from Java 8 installation
+ */
+ @Nullable
+ private Path findRtJarPath() {
+ String userDefinedRtJarPath = System.getProperty("rtJarPath");
+ if (userDefinedRtJarPath != null) {
+ return Path.of(userDefinedRtJarPath);
+ }
+
+ Optional javaInstall = JavaEnv.getJavaInstalls().stream()
+ .filter(javaInstall1 -> javaInstall1.version() == 8)
+ .findFirst();
+
+ if (javaInstall.isPresent()) {
+ JavaInstall install = javaInstall.get();
+ Path possibleRtJarPath = install.javaExecutable().getParent().getParent().resolve("jre").resolve("lib").resolve("rt.jar");
+ if (Files.exists(possibleRtJarPath)) {
+ return possibleRtJarPath;
+ }
+ }
+ return null;
+ }
+
/**
* Build immutable {@link DeobfuscatorOptions} with options verification
*/
@@ -269,12 +316,23 @@ public DeobfuscatorOptions build() {
if (this.outputJar != null && this.outputDir != null) {
throw new IllegalStateException("Output jar and output dir cannot be set at the same time");
}
+ // Try to auto-detect rt.jar path
+ if (this.rtJarPath == null) {
+ Path rtJar = findRtJarPath();
+ if (rtJar != null) {
+ System.out.println("Auto-detected rt.jar path: " + rtJar);
+ this.rtJarPath = rtJar;
+ } else {
+ LOGGER.warn("Failed to auto-detect rt.jar path. Please provide path to rt.jar from Java 8 binaries, otherwise sandbox will not work.");
+ }
+ }
return new DeobfuscatorOptions(
// Input
inputJar,
externalFiles,
libraries,
+ rtJarPath,
// Output
outputJar,
outputDir,
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaEnv.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaEnv.java
new file mode 100644
index 00000000..c40760d6
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaEnv.java
@@ -0,0 +1,324 @@
+package uwu.narumi.deobfuscator.api.environment;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import uwu.narumi.deobfuscator.api.helper.PlatformType;
+import uwu.narumi.deobfuscator.api.helper.SymLinks;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Tasks for Java environments.
+ *
+ * https://github.com/Col-E/Recaf-Launcher/blob/master/core/src/main/java/software/coley/recaf/launcher/task/JavaEnvTasks.java
+ */
+public class JavaEnv {
+ private static final Set javaInstalls = new HashSet<>();
+
+ /**
+ * Must call {@link #scanForJavaInstalls()} before this list will be populated.
+ *
+ * @return Set of discovered Java installations.
+ */
+ @NotNull
+ public static Collection getJavaInstalls() {
+ return javaInstalls;
+ }
+
+ static {
+ // On class-load, scan for java installs.
+ scanForJavaInstalls();
+ }
+
+ /**
+ * Detect common Java installations for the current platform.
+ */
+ private static void scanForJavaInstalls() {
+ if (PlatformType.isWindows()) {
+ scanForWindowsJavaPaths();
+ } else if (PlatformType.isLinux()) {
+ scanForLinuxJavaPaths();
+ } else if (PlatformType.isMac()) {
+ scanforMacJavaPaths();
+ }
+ }
+
+ /**
+ * Detect common Java installations on Linux.
+ */
+ private static void scanForLinuxJavaPaths() {
+ // Check java alternative link.
+ Path altJava = Paths.get("/etc/alternatives/java");
+ if (Files.exists(altJava)) {
+ addJavaInstall(altJava);
+ }
+
+ // Check home
+ String homeEnv = System.getenv("JAVA_HOME");
+ if (homeEnv != null) {
+ Path homePath = Paths.get(homeEnv);
+ if (Files.isDirectory(homePath)) {
+ Path javaPath = homePath.resolve("bin/java");
+ if (Files.exists(javaPath))
+ addJavaInstall(javaPath);
+ }
+ }
+
+ // Check common install locations.
+ String[] javaRoots = {
+ "/usr/lib/jvm/",
+ System.getenv("HOME") + "/.jdks/"
+ };
+ for (String root : javaRoots) {
+ Path rootPath = Paths.get(root);
+ if (Files.isDirectory(rootPath)) {
+ try (Stream subDirStream = Files.list(rootPath)) {
+ subDirStream.filter(subDir -> Files.exists(subDir.resolve("bin/java")))
+ .forEach(subDir -> {
+ Path javaPath = subDir.resolve("bin/java");
+ if (Files.exists(javaPath))
+ addJavaInstall(javaPath);
+ });
+ } catch (IOException ignored) {
+ // Skip
+ }
+ }
+ }
+ }
+
+ /**
+ * Detect common Java installations on Mac.
+ */
+ private static void scanforMacJavaPaths() {
+ Path[] jvmsRoots = new Path[]{
+ Paths.get("/Library/Java/JavaVirtualMachines/"),
+ Paths.get(System.getProperty("user.home")).resolve("Library/Java/JavaVirtualMachines/")
+ };
+ for (Path jvmsRoot : jvmsRoots) {
+ if (Files.isDirectory(jvmsRoot)) {
+ try (Stream stream = Files.walk(jvmsRoot)) {
+ stream.forEach(path -> {
+ if (path.toString().endsWith("bin/java"))
+ addJavaMacInstall(path);
+ });
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Detect common Java installations on Windows.
+ */
+ private static void scanForWindowsJavaPaths() {
+ String homeProp = System.getProperty("java.home");
+ if (homeProp != null)
+ addJavaInstall(Paths.get(homeProp).resolve("bin/java.exe"));
+
+ // Check java home
+ String homeEnv = System.getenv("JAVA_HOME");
+ if (homeEnv != null) {
+ Path homePath = Paths.get(homeEnv);
+ if (Files.isDirectory(homePath))
+ addJavaInstall(homePath.resolve("bin/java.exe"));
+ }
+
+ // Check '%user%/.jdks'
+ String homePath = System.getProperty("user.home");
+ if (homePath != null) {
+ Path jdksDir = Paths.get(homePath, ".jdks");
+ if (Files.isDirectory(jdksDir))
+ try (Stream subDirStream = Files.list(jdksDir)) {
+ subDirStream.filter(subDir -> Files.exists(subDir.resolve("bin/java.exe")))
+ .forEach(subDir -> addJavaInstall(subDir.resolve("bin/java.exe")));
+ } catch (IOException ignored) {
+ // Skip
+ }
+ }
+
+ // Check system path for java entries.
+ String path = System.getenv("PATH");
+ if (path != null) {
+ String[] entries = path.split(";");
+ for (String entry : entries)
+ if (entry.endsWith("bin"))
+ addJavaInstall(Paths.get(entry).resolve("java.exe"));
+ }
+
+ // Check common install locations.
+ String[] javaRoots = {
+ "C:/Program Files/Amazon Corretto/",
+ "C:/Program Files/Eclipse Adoptium/",
+ "C:/Program Files/Eclipse Foundation/",
+ "C:/Program Files/BellSoft/",
+ "C:/Program Files/Java/",
+ "C:/Program Files/Microsoft/",
+ "C:/Program Files/SapMachine/JDK/",
+ "C:/Program Files/Zulu/",
+ };
+ for (String root : javaRoots) {
+ Path rootPath = Paths.get(root);
+ if (Files.isDirectory(rootPath)) {
+ try (Stream subDirStream = Files.list(rootPath)) {
+ subDirStream.filter(subDir -> Files.exists(subDir.resolve("bin/java.exe")))
+ .forEach(subDir -> addJavaInstall(subDir.resolve("bin/java.exe")));
+ } catch (IOException ignored) {
+ // Skip
+ }
+ }
+ }
+ }
+
+ /**
+ * @param javaExecutable Path to executable to add.
+ * @return {@code true} when the path was recognized as a valid executable.
+ * {@code false} when discarded.
+ */
+ @NotNull
+ public static AdditionResult addJavaInstall(@NotNull Path javaExecutable) {
+ return addJavaInstall(javaExecutable, executable -> {
+ // Most installs are structured like: /whatever/jvms/openjdk-21.0.3/bin/java.exe
+ // Thus, the parent of the bin directory has the name.
+ Path binDir = executable.getParent();
+ if (binDir == null)
+ return null;
+ Path jdkDir = binDir.getParent();
+ if (jdkDir == null)
+ return null;
+ return jdkDir.getFileName().toString();
+ });
+ }
+
+ /**
+ * @param javaExecutable Path to executable to add.
+ * @return {@code true} when the path was recognized as a valid executable.
+ * {@code false} when discarded.
+ */
+ @NotNull
+ public static AdditionResult addJavaMacInstall(@NotNull Path javaExecutable) {
+ return addJavaInstall(javaExecutable, executable -> {
+ // Mac structures things differently: /Library/Java/JavaVirtualMachines/openjdk-21.0.3.jdk/Contents/Home/bin/java.exe
+ // Thus, going up 4 directory levels will reveal the name.
+ Path binDir = executable.getParent();
+ if (binDir == null)
+ return null;
+ Path jdkHomeDir = binDir.getParent();
+ if (jdkHomeDir == null)
+ return null;
+ Path jdkContentsDir = jdkHomeDir.getParent();
+ if (jdkContentsDir == null)
+ return null;
+ Path jdkDir = jdkContentsDir.getParent();
+ if (jdkDir == null)
+ return null;
+ return jdkDir.getFileName().toString();
+ });
+ }
+
+ /**
+ * @param javaExecutable Path to executable to add.
+ * @param executableToJvmName Lookup to find JDK name from the path of the executable.
+ * @return {@code true} when the path was recognized as a valid executable.
+ * {@code false} when discarded.
+ */
+ @NotNull
+ public static AdditionResult addJavaInstall(@NotNull Path javaExecutable, @NotNull Function executableToJvmName) {
+ // Resolve sym-links
+ if (Files.isSymbolicLink(javaExecutable)) {
+ javaExecutable = SymLinks.resolveSymLink(javaExecutable);
+ if (javaExecutable == null)
+ return AdditionResult.ERR_RESOLVE_SYM_LINK;
+ }
+
+ // Validate executable is 'java' or 'javaw'
+ String execName = javaExecutable.getFileName().toString();
+ if (!execName.endsWith("java") && !javaExecutable.endsWith("java.exe")
+ && !execName.endsWith("javaw") && !javaExecutable.endsWith("javaw.exe"))
+ return AdditionResult.ERR_NOT_JAVA_EXEC;
+
+ // Validate the given path points to a file that exists
+ if (!Files.exists(javaExecutable))
+ return AdditionResult.ERR_NOT_JAVA_EXEC;
+
+ // Validate bin structure
+ Path binDir = javaExecutable.getParent();
+ if (binDir == null)
+ return AdditionResult.ERR_PARENT;
+
+ // Validate it's a JDK and not a JRE
+ if (Files.notExists(binDir.resolve("javac")) && Files.notExists(binDir.resolve("javac.exe")))
+ return AdditionResult.ERR_JRE_NOT_JDK;
+
+ // Validate version
+ String jdkDirName = executableToJvmName.apply(javaExecutable);
+ if (jdkDirName == null)
+ return AdditionResult.ERR_PARENT;
+ int version = JavaVersion.fromVersionString(jdkDirName);
+ if (version == JavaVersion.UNKNOWN_VERSION)
+ return AdditionResult.ERR_UNRESOLVED_VERSION;
+ if (version >= 8) {
+ addJavaInstall(new JavaInstall(javaExecutable, version));
+ return AdditionResult.SUCCESS;
+ }
+ return AdditionResult.ERR_TOO_OLD;
+ }
+
+ /**
+ * @param path Path to executable to look up.
+ * @return Install entry for path, or {@code null} if not previously recorded as a valid installation.
+ */
+ @Nullable
+ public static JavaInstall getByPath(@NotNull Path path) {
+ return javaInstalls.stream()
+ .filter(i -> i.javaExecutable().equals(path))
+ .findFirst().orElse(null);
+ }
+
+ private static void addJavaInstall(@NotNull JavaInstall install) {
+ javaInstalls.add(install);
+ }
+
+ public enum AdditionResult {
+ SUCCESS,
+ ERR_NOT_JAVA_EXEC,
+ ERR_RESOLVE_SYM_LINK,
+ ERR_PARENT,
+ ERR_JRE_NOT_JDK,
+ ERR_UNRESOLVED_VERSION,
+ ERR_TOO_OLD;
+
+ public boolean wasSuccess() {
+ return this == SUCCESS;
+ }
+
+ @NotNull
+ public String message() {
+ switch (this) {
+ case SUCCESS:
+ return "";
+ case ERR_RESOLVE_SYM_LINK:
+ return "The selected symbolic-link could not be resolved";
+ case ERR_NOT_JAVA_EXEC:
+ return "The selected file was not 'java' or 'javaw'";
+ case ERR_UNRESOLVED_VERSION:
+ return "The selected java executable could not have its version resolved";
+ case ERR_PARENT:
+ return "The selected java executable could not have its parent directories";
+ case ERR_JRE_NOT_JDK:
+ return "The selected java executable belongs to a JRE and not a JDK";
+ case ERR_TOO_OLD:
+ return "The selected java executable is from a outdated/unsupported version of Java";
+ }
+ return "The selected executable was not valid: " + name();
+ }
+ }
+}
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaInstall.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaInstall.java
new file mode 100644
index 00000000..b31fb617
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaInstall.java
@@ -0,0 +1,29 @@
+package uwu.narumi.deobfuscator.api.environment;
+
+import java.nio.file.Path;
+import java.util.Comparator;
+
+/**
+ * Model of a Java installation.
+ *
+ * @param javaExecutable Path to the Java executable.
+ * @param version Major version of the installation.
+ *
+ * https://github.com/Col-E/Recaf-Launcher/blob/master/core/src/main/java/software/coley/recaf/launcher/info/JavaInstall.java
+ */
+public record JavaInstall(Path javaExecutable, int version) {
+ /**
+ * Compare installs by path.
+ */
+ public static Comparator COMPARE_PATHS = Comparator.comparing(o -> o.javaExecutable);
+ /**
+ * Compare installs by version (newest first).
+ */
+ public static Comparator COMPARE_VERSIONS = (o1, o2) -> {
+ // Negated so newer versions are sorted to be first
+ int cmp = -Integer.compare(o1.version, o2.version);
+ if (cmp == 0)
+ return COMPARE_PATHS.compare(o1, o2);
+ return cmp;
+ };
+}
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaVersion.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaVersion.java
new file mode 100644
index 00000000..4e92fbf2
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/environment/JavaVersion.java
@@ -0,0 +1,73 @@
+package uwu.narumi.deobfuscator.api.environment;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Supported Java version of the current JVM.
+ *
+ * https://github.com/Col-E/Recaf-Launcher/blob/master/core/src/main/java/software/coley/recaf/launcher/info/JavaVersion.java
+ */
+public class JavaVersion {
+ /**
+ * The offset from which a version and the version constant value is. For example, Java 8 is 52 (44 + 8).
+ */
+ public static final int VERSION_OFFSET = 44;
+ /**
+ * Code indicator that we couldn't figure out the version.
+ */
+ public static final int UNKNOWN_VERSION = -2;
+ /**
+ * Regex pattern which extracts the major release version from a string.
+ * Ignores most common suffix/prefix patterns.
+ */
+ private static final Pattern JAVA_VERSION_EXTRACTOR = Pattern.compile("(?:(?:[^\\d\\W]|[- ])+)?(?:1\\D)?(\\d+)(?:_.+)?(?:\\..+)?");
+ private static final String JAVA_CLASS_VERSION = "java.class.version";
+ private static final String JAVA_VM_SPEC_VERSION = "java.vm.specification.version";
+ private static int version = -1;
+
+ /**
+ * @param version
+ * Version string.
+ *
+ * @return Version if parsable, otherwise {@link #UNKNOWN_VERSION}.
+ */
+ public static int fromVersionString(@NotNull String version) {
+ try {
+ Matcher matcher = JAVA_VERSION_EXTRACTOR.matcher(version);
+ if (matcher.find())
+ return Integer.parseInt(matcher.group(1));
+ } catch (Exception ignored) {
+ // ignored
+ }
+ return UNKNOWN_VERSION;
+ }
+
+ /**
+ * Get the supported Java version of the current JVM.
+ *
+ * @return Version. If normal detection means do not suffice, then {@link #UNKNOWN_VERSION}.
+ */
+ public static int get() {
+ if (version == -1) {
+ // Check for class version
+ String property = System.getProperty(JAVA_CLASS_VERSION, "");
+ if (!property.isEmpty())
+ return version = (int) (Float.parseFloat(property) - VERSION_OFFSET);
+
+ // Odd, not found. Try the spec version
+ property = System.getProperty(JAVA_VM_SPEC_VERSION, "");
+ if (property.contains("."))
+ return version = (int) Float.parseFloat(property.substring(property.indexOf('.') + 1));
+ else if (!property.isEmpty())
+ return version = Integer.parseInt(property);
+
+ // Very odd
+ return version = UNKNOWN_VERSION;
+ }
+ return version;
+ }
+}
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/JarBootClassFinder.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/JarBootClassFinder.java
new file mode 100644
index 00000000..6ce014d0
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/JarBootClassFinder.java
@@ -0,0 +1,49 @@
+package uwu.narumi.deobfuscator.api.execution;
+
+import dev.xdark.ssvm.classloading.BootClassFinder;
+import dev.xdark.ssvm.classloading.ParsedClassData;
+import dev.xdark.ssvm.util.ClassUtil;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.tree.ClassNode;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * A {@link BootClassFinder} that finds classes in a given rt.jar file.
+ */
+public class JarBootClassFinder implements BootClassFinder {
+ private final JarFile rtJarFile;
+
+ public JarBootClassFinder(Path rtJarPath) {
+ try {
+ this.rtJarFile = new JarFile(rtJarPath.toFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ParsedClassData findBootClass(String name) {
+ // Find class in the rt.jar
+ JarEntry jarEntry = this.rtJarFile.getJarEntry(name + ".class");
+ if (jarEntry == null) {
+ return null;
+ }
+
+ ClassReader cr;
+ try (InputStream in = this.rtJarFile.getInputStream(jarEntry)) {
+ if (in == null) {
+ return null;
+ }
+ cr = new ClassReader(in);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ ClassNode node = ClassUtil.readNode(cr);
+ return new ParsedClassData(cr, node);
+ }
+}
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java
index 79282c69..cdf397ae 100644
--- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java
@@ -4,6 +4,7 @@
import dev.xdark.ssvm.RuntimeResolver;
import dev.xdark.ssvm.VirtualMachine;
import dev.xdark.ssvm.api.VMInterface;
+import dev.xdark.ssvm.classloading.BootClassFinder;
import dev.xdark.ssvm.classloading.SupplyingClassLoaderInstaller;
import dev.xdark.ssvm.execution.ExecutionEngine;
import dev.xdark.ssvm.filesystem.FileManager;
@@ -16,6 +17,7 @@
import dev.xdark.ssvm.thread.ThreadManager;
import dev.xdark.ssvm.util.Reflection;
+import java.nio.file.Path;
import java.util.List;
import org.apache.logging.log4j.LogManager;
@@ -41,11 +43,24 @@ public SandBox(Context context) {
this(new ClasspathDataSupplier(
// We need to use compiled classes as they are already compiled
new CombinedClassProvider(context.getCompiledClasses(), context.getLibraries())
- ));
- }
-
- public SandBox(SupplyingClassLoaderInstaller.DataSupplier dataSupplier) {
- this(dataSupplier, new VirtualMachine());
+ ), context.getOptions().rtJarPath());
+ }
+
+ public SandBox(SupplyingClassLoaderInstaller.DataSupplier dataSupplier, Path rtJarPath) {
+ this(dataSupplier, new VirtualMachine() {
+ @Override
+ protected BootClassFinder createBootClassFinder() {
+ if (rtJarPath == null) {
+ throw new IllegalStateException("rt.jar is required for sandbox to run. Please see README.md for instructions");
+ }
+ return new JarBootClassFinder(rtJarPath);
+ }
+
+ @Override
+ public int getJvmVersion() {
+ return 8; // Java 8
+ }
+ });
}
public SandBox(SupplyingClassLoaderInstaller.DataSupplier dataSupplier, VirtualMachine vm) {
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/PlatformType.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/PlatformType.java
new file mode 100644
index 00000000..32eecc7a
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/PlatformType.java
@@ -0,0 +1,45 @@
+package uwu.narumi.deobfuscator.api.helper;
+
+/**
+ * Operating system enumeration.
+ *
+ * https://github.com/Col-E/Recaf-Launcher/blob/master/core/src/main/java/software/coley/recaf/launcher/info/PlatformType.java
+ */
+public enum PlatformType {
+ WINDOWS,
+ MAC,
+ LINUX;
+
+ /**
+ * @return {@code true} when the current platform is windows.
+ */
+ public static boolean isWindows() {
+ return get() == WINDOWS;
+ }
+
+ /**
+ * @return {@code true} when the current platform is mac.
+ */
+ public static boolean isMac() {
+ return get() == MAC;
+ }
+
+ /**
+ * @return {@code true} when the current platform is linux.
+ */
+ public static boolean isLinux() {
+ return get() == LINUX;
+ }
+
+ /**
+ * @return Operating system type.
+ */
+ public static PlatformType get() {
+ String osName = System.getProperty("os.name").toLowerCase();
+ if (osName.contains("win"))
+ return WINDOWS;
+ if (osName.contains("mac") || osName.contains("osx"))
+ return MAC;
+ return LINUX;
+ }
+}
diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/SymLinks.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/SymLinks.java
new file mode 100644
index 00000000..7076355e
--- /dev/null
+++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/SymLinks.java
@@ -0,0 +1,39 @@
+package uwu.narumi.deobfuscator.api.helper;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Symbolic link utils
+ *
+ * https://github.com/Col-E/Recaf-Launcher/blob/master/core/src/main/java/software/coley/recaf/launcher/util/SymLinks.java
+ */
+public class SymLinks {
+ private static final int MAX_LINK_DEPTH = 10;
+
+ /**
+ * @param path
+ * Symbolic link to follow.
+ *
+ * @return Target path, or {@code null} if the path could not be resolved.
+ */
+ @Nullable
+ public static Path resolveSymLink(@NotNull Path path) {
+ try {
+ int linkDepth = 0;
+ while (Files.isSymbolicLink(path)) {
+ if (linkDepth > MAX_LINK_DEPTH)
+ throw new IOException("Sym-link path too deep");
+ path = Files.readSymbolicLink(path);
+ linkDepth++;
+ }
+ return path;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+}