diff --git a/dev.skidfuscator.obfuscator.dependanalysis/build.gradle b/dev.skidfuscator.obfuscator.dependanalysis/build.gradle new file mode 100644 index 0000000..31f3355 --- /dev/null +++ b/dev.skidfuscator.obfuscator.dependanalysis/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +group = 'dev.skidfuscator.community' +version = '2.0.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + api project(':modasm') + + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyAnalyzer.java b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyAnalyzer.java new file mode 100644 index 0000000..d8e7978 --- /dev/null +++ b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyAnalyzer.java @@ -0,0 +1,218 @@ +package dev.skidfuscator.dependanalysis; + +import dev.skidfuscator.dependanalysis.visitor.HierarchyVisitor; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Due to the nature of the Java Virtual Machine and the wealth of tools offered by OW2 ASM, we can + * analyze the hierarchy of classes within a given JAR and selectively load only the minimal set of + * dependencies required. By parsing the main JAR’s class definitions and walking up its hierarchy + * chain, we identify which subset of external JARs is truly needed. + * + * This class orchestrates the resolution by: + * - Indexing the classes found in a directory of library JARs. + * - Parsing the main JAR’s classes and discovering their superclasses and implemented interfaces. + * - Recursively climbing the class hierarchy to find any library JAR that must be included. + */ +public class DependencyAnalyzer { + private final Path mainJar; + private final Path librariesDir; + + // Maps className to the jar that hosts it (for library jars) + private final Map classToLibraryMap = new HashMap<>(); + // Cache of previously computed hierarchies to avoid re-analysis + private final Map classHierarchyCache = new HashMap<>(); + + public DependencyAnalyzer(Path mainJar, Path librariesDir) { + this.mainJar = mainJar; + this.librariesDir = librariesDir; + } + + /** + * Analyze the main jar’s classes, build their hierarchies, and return + * the minimal set of library jars required to resolve the entire chain. + */ + public Set analyze() throws IOException { + // Step 1: Index library jars + indexLibraries(); + + // Step 2: Get all classes from main jar + Set mainClasses = loadClassesFromJar(mainJar); + + // Step 3: Resolve hierarchical dependencies + Set requiredJars = new HashSet<>(); + for (String cls : mainClasses) { + resolveHierarchy(cls, requiredJars, mainJar, new HashSet<>()); + } + return requiredJars; + } + + /** + * Recursively resolves the hierarchy of a given class, adding necessary jars as discovered. + */ + private void resolveHierarchy(String className, Set requiredJars, Path sourceJar, Set visited) throws IOException { + if (visited.contains(className)) return; + visited.add(className); + + DependencyClassHierarchy hierarchy = loadClassHierarchy(className, sourceJar); + + // If we found a class from a library jar + if (!hierarchy.isMainJarClass && hierarchy.sourceJar != null) { + requiredJars.add(hierarchy.sourceJar); + } + + // Resolve superclass + if (hierarchy.superName != null && !hierarchy.superName.isEmpty()) { + Path jarForSuper = hierarchy.isMainJarClass ? mainJar : classToLibraryMap.get(hierarchy.superName); + if (jarForSuper == null && hierarchy.superName != null) { + jarForSuper = classToLibraryMap.get(hierarchy.superName); + } + if (jarForSuper != null) { + resolveHierarchy(hierarchy.superName, requiredJars, jarForSuper, visited); + } + } + + // Resolve interfaces + for (String iface : hierarchy.interfaces) { + Path jarForIface = hierarchy.isMainJarClass ? mainJar : classToLibraryMap.get(iface); + if (jarForIface == null && iface != null) { + jarForIface = classToLibraryMap.get(iface); + } + if (jarForIface != null) { + resolveHierarchy(iface, requiredJars, jarForIface, visited); + } + } + } + + /** + * Load the class hierarchy for a given class. If cached, return the cache. + * Otherwise, parse from either the main jar or a known library jar. + */ + private DependencyClassHierarchy loadClassHierarchy(String className, Path presumedJar) throws IOException { + if (classHierarchyCache.containsKey(className)) { + return classHierarchyCache.get(className); + } + + boolean fromMainJar = false; + InputStream classStream = getClassStream(mainJar, className); + Path jarSource = null; + if (classStream != null) { + fromMainJar = true; + jarSource = mainJar; + } else { + Path libJar = classToLibraryMap.get(className); + if (libJar == null) { + // Not found in known jars + DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null); + classHierarchyCache.put(className, notFound); + return notFound; + } + classStream = getClassStream(libJar, className); + jarSource = libJar; + } + + if (classStream == null) { + DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null); + classHierarchyCache.put(className, notFound); + return notFound; + } + + ClassReader cr = new ClassReader(classStream); + HierarchyVisitor visitor = new HierarchyVisitor(); + cr.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE); + + DependencyClassHierarchy hierarchy = new DependencyClassHierarchy( + className, + visitor.superName, + visitor.interfaces, + fromMainJar, + fromMainJar ? null : jarSource + ); + classHierarchyCache.put(className, hierarchy); + return hierarchy; + } + + /** + * Index all library jars found in librariesDir by their contained classes. + */ + private void indexLibraries() throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(librariesDir, "*.jar")) { + for (Path jar : stream) { + try (JarFile jarFile = new JarFile(jar.toFile())) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (!entry.isDirectory() && entry.getName().endsWith(".class")) { + String className = entry.getName().replace('/', '.').replace(".class", ""); + classToLibraryMap.put(className, jar); + } + } + } + } + } + } + + /** + * Load all classes from a given jar. + */ + private Set loadClassesFromJar(Path jarPath) throws IOException { + Set classes = new HashSet<>(); + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (!entry.isDirectory() && entry.getName().endsWith(".class")) { + String className = entry.getName().replace('/', '.').replace(".class", ""); + classes.add(className); + } + } + } + return classes; + } + + /** + * Retrieve an InputStream for a specified class from a given jar. + */ + private InputStream getClassStream(Path jar, String className) throws IOException { + // Need a fresh stream for each read attempt + JarFile jf = new JarFile(jar.toFile()); + String path = className.replace('.', '/') + ".class"; + JarEntry entry = jf.getJarEntry(path); + if (entry == null) { + jf.close(); + return null; + } + return new ClosableInputStreamWrapper(jf, jf.getInputStream(entry)); + } + + /** + * A wrapper that closes the JarFile once the InputStream is closed. + */ + private static class ClosableInputStreamWrapper extends InputStream { + private final JarFile jarFile; + private final InputStream delegate; + + public ClosableInputStreamWrapper(JarFile jarFile, InputStream delegate) { + this.jarFile = jarFile; + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public void close() throws IOException { + delegate.close(); + jarFile.close(); + } + } +} diff --git a/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyClassHierarchy.java b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyClassHierarchy.java new file mode 100644 index 0000000..0285755 --- /dev/null +++ b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyClassHierarchy.java @@ -0,0 +1,27 @@ +package dev.skidfuscator.dependanalysis; + +import java.nio.file.Path; + +/** + * A simple data structure holding the hierarchy information of a single class: + * - The class name + * - Its immediate superclass name + * - Any interfaces it implements + * - Whether it was found in the main jar or a library jar + * - The source jar path (if from a library) + */ +public class DependencyClassHierarchy { + public final String className; + public final String superName; + public final String[] interfaces; + public final boolean isMainJarClass; + public final Path sourceJar; + + public DependencyClassHierarchy(String className, String superName, String[] interfaces, boolean isMainJarClass, Path sourceJar) { + this.className = className; + this.superName = superName; + this.interfaces = interfaces; + this.isMainJarClass = isMainJarClass; + this.sourceJar = sourceJar; + } +} diff --git a/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/Main.java b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/Main.java new file mode 100644 index 0000000..43636aa --- /dev/null +++ b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/Main.java @@ -0,0 +1,32 @@ +package dev.skidfuscator.dependanalysis; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; + +/** + * A simple entry point for demonstration purposes. Provide: + * java com.example.dependencyanalyzer.Main + * + * For instance: + * java com.example.dependencyanalyzer.Main my-app.jar libs/ + */ +public class Main { + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("Usage: java com.example.dependencyanalyzer.Main "); + System.exit(1); + } + + Path mainJar = Paths.get(args[0]); + Path libs = Paths.get(args[1]); + + DependencyAnalyzer analyzer = new DependencyAnalyzer(mainJar, libs); + Set requiredJars = analyzer.analyze(); + + System.out.println("Required jars:"); + for (Path jar : requiredJars) { + System.out.println(" - " + jar); + } + } +} diff --git a/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/visitor/HierarchyVisitor.java b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/visitor/HierarchyVisitor.java new file mode 100644 index 0000000..51060cf --- /dev/null +++ b/dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/visitor/HierarchyVisitor.java @@ -0,0 +1,32 @@ +package dev.skidfuscator.dependanalysis.visitor; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +/** + * A ClassVisitor used to extract the superclass and interface names from a given class. + * It normalizes slashes in class names to dot notation. + */ +public class HierarchyVisitor extends ClassVisitor { + public String superName; + public String[] interfaces = new String[0]; + + public HierarchyVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (superName != null) { + this.superName = superName.replace('/', '.'); + } + if (interfaces != null) { + String[] replaced = new String[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + replaced[i] = interfaces[i].replace('/', '.'); + } + this.interfaces = replaced; + } + super.visit(version, access, name, signature, superName, interfaces); + } +} diff --git a/settings.gradle b/settings.gradle index e6bfaf2..bfe7fe7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,6 +40,7 @@ void initBase() { include(':annotations') include ':commons' include ':pure-analysis' + include ':depend-analysis' include ':obfuscator' include ':client-standalone' include ':gradle-plugin' @@ -47,6 +48,7 @@ void initBase() { project(":annotations").projectDir = file('dev.skidfuscator.annotations') project(":commons").projectDir = file('dev.skidfuscator.commons') project(":pure-analysis").projectDir = file('dev.skidfuscator.obfuscator.pureanalysis') + project(":depend-analysis").projectDir = file('dev.skidfuscator.obfuscator.dependanalysis') project(":obfuscator").projectDir = file('dev.skidfuscator.obfuscator') project(":client-standalone").projectDir = file('dev.skidfuscator.client.standalone') project(":gradle-plugin").projectDir = file('dev.skidfuscator.gradle-plugin')