Skip to content

Commit bb1ba98

Browse files
committed
feat: dependency minimizer
1 parent c755628 commit bb1ba98

File tree

6 files changed

+333
-0
lines changed

6 files changed

+333
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
group = 'dev.skidfuscator.community'
6+
version = '2.0.0-SNAPSHOT'
7+
8+
repositories {
9+
mavenCentral()
10+
maven { url 'https://jitpack.io' }
11+
}
12+
13+
dependencies {
14+
api project(':modasm')
15+
16+
testImplementation platform('org.junit:junit-bom:5.10.0')
17+
testImplementation 'org.junit.jupiter:junit-jupiter'
18+
}
19+
20+
test {
21+
useJUnitPlatform()
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package dev.skidfuscator.dependanalysis;
2+
3+
import dev.skidfuscator.dependanalysis.visitor.HierarchyVisitor;
4+
import org.objectweb.asm.ClassReader;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.nio.file.*;
9+
import java.util.*;
10+
import java.util.jar.JarEntry;
11+
import java.util.jar.JarFile;
12+
13+
/**
14+
* Due to the nature of the Java Virtual Machine and the wealth of tools offered by OW2 ASM, we can
15+
* analyze the hierarchy of classes within a given JAR and selectively load only the minimal set of
16+
* dependencies required. By parsing the main JAR’s class definitions and walking up its hierarchy
17+
* chain, we identify which subset of external JARs is truly needed.
18+
*
19+
* This class orchestrates the resolution by:
20+
* - Indexing the classes found in a directory of library JARs.
21+
* - Parsing the main JAR’s classes and discovering their superclasses and implemented interfaces.
22+
* - Recursively climbing the class hierarchy to find any library JAR that must be included.
23+
*/
24+
public class DependencyAnalyzer {
25+
private final Path mainJar;
26+
private final Path librariesDir;
27+
28+
// Maps className to the jar that hosts it (for library jars)
29+
private final Map<String, Path> classToLibraryMap = new HashMap<>();
30+
// Cache of previously computed hierarchies to avoid re-analysis
31+
private final Map<String, DependencyClassHierarchy> classHierarchyCache = new HashMap<>();
32+
33+
public DependencyAnalyzer(Path mainJar, Path librariesDir) {
34+
this.mainJar = mainJar;
35+
this.librariesDir = librariesDir;
36+
}
37+
38+
/**
39+
* Analyze the main jar’s classes, build their hierarchies, and return
40+
* the minimal set of library jars required to resolve the entire chain.
41+
*/
42+
public Set<Path> analyze() throws IOException {
43+
// Step 1: Index library jars
44+
indexLibraries();
45+
46+
// Step 2: Get all classes from main jar
47+
Set<String> mainClasses = loadClassesFromJar(mainJar);
48+
49+
// Step 3: Resolve hierarchical dependencies
50+
Set<Path> requiredJars = new HashSet<>();
51+
for (String cls : mainClasses) {
52+
resolveHierarchy(cls, requiredJars, mainJar, new HashSet<>());
53+
}
54+
return requiredJars;
55+
}
56+
57+
/**
58+
* Recursively resolves the hierarchy of a given class, adding necessary jars as discovered.
59+
*/
60+
private void resolveHierarchy(String className, Set<Path> requiredJars, Path sourceJar, Set<String> visited) throws IOException {
61+
if (visited.contains(className)) return;
62+
visited.add(className);
63+
64+
DependencyClassHierarchy hierarchy = loadClassHierarchy(className, sourceJar);
65+
66+
// If we found a class from a library jar
67+
if (!hierarchy.isMainJarClass && hierarchy.sourceJar != null) {
68+
requiredJars.add(hierarchy.sourceJar);
69+
}
70+
71+
// Resolve superclass
72+
if (hierarchy.superName != null && !hierarchy.superName.isEmpty()) {
73+
Path jarForSuper = hierarchy.isMainJarClass ? mainJar : classToLibraryMap.get(hierarchy.superName);
74+
if (jarForSuper == null && hierarchy.superName != null) {
75+
jarForSuper = classToLibraryMap.get(hierarchy.superName);
76+
}
77+
if (jarForSuper != null) {
78+
resolveHierarchy(hierarchy.superName, requiredJars, jarForSuper, visited);
79+
}
80+
}
81+
82+
// Resolve interfaces
83+
for (String iface : hierarchy.interfaces) {
84+
Path jarForIface = hierarchy.isMainJarClass ? mainJar : classToLibraryMap.get(iface);
85+
if (jarForIface == null && iface != null) {
86+
jarForIface = classToLibraryMap.get(iface);
87+
}
88+
if (jarForIface != null) {
89+
resolveHierarchy(iface, requiredJars, jarForIface, visited);
90+
}
91+
}
92+
}
93+
94+
/**
95+
* Load the class hierarchy for a given class. If cached, return the cache.
96+
* Otherwise, parse from either the main jar or a known library jar.
97+
*/
98+
private DependencyClassHierarchy loadClassHierarchy(String className, Path presumedJar) throws IOException {
99+
if (classHierarchyCache.containsKey(className)) {
100+
return classHierarchyCache.get(className);
101+
}
102+
103+
boolean fromMainJar = false;
104+
InputStream classStream = getClassStream(mainJar, className);
105+
Path jarSource = null;
106+
if (classStream != null) {
107+
fromMainJar = true;
108+
jarSource = mainJar;
109+
} else {
110+
Path libJar = classToLibraryMap.get(className);
111+
if (libJar == null) {
112+
// Not found in known jars
113+
DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null);
114+
classHierarchyCache.put(className, notFound);
115+
return notFound;
116+
}
117+
classStream = getClassStream(libJar, className);
118+
jarSource = libJar;
119+
}
120+
121+
if (classStream == null) {
122+
DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null);
123+
classHierarchyCache.put(className, notFound);
124+
return notFound;
125+
}
126+
127+
ClassReader cr = new ClassReader(classStream);
128+
HierarchyVisitor visitor = new HierarchyVisitor();
129+
cr.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE);
130+
131+
DependencyClassHierarchy hierarchy = new DependencyClassHierarchy(
132+
className,
133+
visitor.superName,
134+
visitor.interfaces,
135+
fromMainJar,
136+
fromMainJar ? null : jarSource
137+
);
138+
classHierarchyCache.put(className, hierarchy);
139+
return hierarchy;
140+
}
141+
142+
/**
143+
* Index all library jars found in librariesDir by their contained classes.
144+
*/
145+
private void indexLibraries() throws IOException {
146+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(librariesDir, "*.jar")) {
147+
for (Path jar : stream) {
148+
try (JarFile jarFile = new JarFile(jar.toFile())) {
149+
Enumeration<JarEntry> entries = jarFile.entries();
150+
while (entries.hasMoreElements()) {
151+
JarEntry entry = entries.nextElement();
152+
if (!entry.isDirectory() && entry.getName().endsWith(".class")) {
153+
String className = entry.getName().replace('/', '.').replace(".class", "");
154+
classToLibraryMap.put(className, jar);
155+
}
156+
}
157+
}
158+
}
159+
}
160+
}
161+
162+
/**
163+
* Load all classes from a given jar.
164+
*/
165+
private Set<String> loadClassesFromJar(Path jarPath) throws IOException {
166+
Set<String> classes = new HashSet<>();
167+
try (JarFile jarFile = new JarFile(jarPath.toFile())) {
168+
Enumeration<JarEntry> entries = jarFile.entries();
169+
while (entries.hasMoreElements()) {
170+
JarEntry entry = entries.nextElement();
171+
if (!entry.isDirectory() && entry.getName().endsWith(".class")) {
172+
String className = entry.getName().replace('/', '.').replace(".class", "");
173+
classes.add(className);
174+
}
175+
}
176+
}
177+
return classes;
178+
}
179+
180+
/**
181+
* Retrieve an InputStream for a specified class from a given jar.
182+
*/
183+
private InputStream getClassStream(Path jar, String className) throws IOException {
184+
// Need a fresh stream for each read attempt
185+
JarFile jf = new JarFile(jar.toFile());
186+
String path = className.replace('.', '/') + ".class";
187+
JarEntry entry = jf.getJarEntry(path);
188+
if (entry == null) {
189+
jf.close();
190+
return null;
191+
}
192+
return new ClosableInputStreamWrapper(jf, jf.getInputStream(entry));
193+
}
194+
195+
/**
196+
* A wrapper that closes the JarFile once the InputStream is closed.
197+
*/
198+
private static class ClosableInputStreamWrapper extends InputStream {
199+
private final JarFile jarFile;
200+
private final InputStream delegate;
201+
202+
public ClosableInputStreamWrapper(JarFile jarFile, InputStream delegate) {
203+
this.jarFile = jarFile;
204+
this.delegate = delegate;
205+
}
206+
207+
@Override
208+
public int read() throws IOException {
209+
return delegate.read();
210+
}
211+
212+
@Override
213+
public void close() throws IOException {
214+
delegate.close();
215+
jarFile.close();
216+
}
217+
}
218+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.skidfuscator.dependanalysis;
2+
3+
import java.nio.file.Path;
4+
5+
/**
6+
* A simple data structure holding the hierarchy information of a single class:
7+
* - The class name
8+
* - Its immediate superclass name
9+
* - Any interfaces it implements
10+
* - Whether it was found in the main jar or a library jar
11+
* - The source jar path (if from a library)
12+
*/
13+
public class DependencyClassHierarchy {
14+
public final String className;
15+
public final String superName;
16+
public final String[] interfaces;
17+
public final boolean isMainJarClass;
18+
public final Path sourceJar;
19+
20+
public DependencyClassHierarchy(String className, String superName, String[] interfaces, boolean isMainJarClass, Path sourceJar) {
21+
this.className = className;
22+
this.superName = superName;
23+
this.interfaces = interfaces;
24+
this.isMainJarClass = isMainJarClass;
25+
this.sourceJar = sourceJar;
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.skidfuscator.dependanalysis;
2+
3+
import java.nio.file.Path;
4+
import java.nio.file.Paths;
5+
import java.util.Set;
6+
7+
/**
8+
* A simple entry point for demonstration purposes. Provide:
9+
* java com.example.dependencyanalyzer.Main <main.jar> <lib_folder>
10+
*
11+
* For instance:
12+
* java com.example.dependencyanalyzer.Main my-app.jar libs/
13+
*/
14+
public class Main {
15+
public static void main(String[] args) throws Exception {
16+
if (args.length < 2) {
17+
System.err.println("Usage: java com.example.dependencyanalyzer.Main <main.jar> <lib_folder>");
18+
System.exit(1);
19+
}
20+
21+
Path mainJar = Paths.get(args[0]);
22+
Path libs = Paths.get(args[1]);
23+
24+
DependencyAnalyzer analyzer = new DependencyAnalyzer(mainJar, libs);
25+
Set<Path> requiredJars = analyzer.analyze();
26+
27+
System.out.println("Required jars:");
28+
for (Path jar : requiredJars) {
29+
System.out.println(" - " + jar);
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.skidfuscator.dependanalysis.visitor;
2+
3+
import org.objectweb.asm.ClassVisitor;
4+
import org.objectweb.asm.Opcodes;
5+
6+
/**
7+
* A ClassVisitor used to extract the superclass and interface names from a given class.
8+
* It normalizes slashes in class names to dot notation.
9+
*/
10+
public class HierarchyVisitor extends ClassVisitor {
11+
public String superName;
12+
public String[] interfaces = new String[0];
13+
14+
public HierarchyVisitor() {
15+
super(Opcodes.ASM9);
16+
}
17+
18+
@Override
19+
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
20+
if (superName != null) {
21+
this.superName = superName.replace('/', '.');
22+
}
23+
if (interfaces != null) {
24+
String[] replaced = new String[interfaces.length];
25+
for (int i = 0; i < interfaces.length; i++) {
26+
replaced[i] = interfaces[i].replace('/', '.');
27+
}
28+
this.interfaces = replaced;
29+
}
30+
super.visit(version, access, name, signature, superName, interfaces);
31+
}
32+
}

settings.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ void initBase() {
4040
include(':annotations')
4141
include ':commons'
4242
include ':pure-analysis'
43+
include ':depend-analysis'
4344
include ':obfuscator'
4445
include ':client-standalone'
4546
include ':gradle-plugin'
4647

4748
project(":annotations").projectDir = file('dev.skidfuscator.annotations')
4849
project(":commons").projectDir = file('dev.skidfuscator.commons')
4950
project(":pure-analysis").projectDir = file('dev.skidfuscator.obfuscator.pureanalysis')
51+
project(":depend-analysis").projectDir = file('dev.skidfuscator.obfuscator.dependanalysis')
5052
project(":obfuscator").projectDir = file('dev.skidfuscator.obfuscator')
5153
project(":client-standalone").projectDir = file('dev.skidfuscator.client.standalone')
5254
project(":gradle-plugin").projectDir = file('dev.skidfuscator.gradle-plugin')

0 commit comments

Comments
 (0)