Skip to content

Commit 2aebc81

Browse files
committed
fix: improve dependency analyser
1 parent d947eb7 commit 2aebc81

File tree

2 files changed

+165
-44
lines changed

2 files changed

+165
-44
lines changed

dev.skidfuscator.obfuscator.dependanalysis/src/main/java/dev/skidfuscator/dependanalysis/DependencyAnalyzer.java

Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
package dev.skidfuscator.dependanalysis;
1+
package com.example.dependencyanalyzer;
22

3+
import dev.skidfuscator.dependanalysis.DependencyClassHierarchy;
4+
import dev.skidfuscator.dependanalysis.DependencyResult;
35
import dev.skidfuscator.dependanalysis.visitor.HierarchyVisitor;
46
import org.objectweb.asm.ClassReader;
57

@@ -11,61 +13,72 @@
1113
import java.util.jar.JarFile;
1214

1315
/**
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.
16+
* The DependencyAnalyzer uses OW2 ASM to determine the minimal set of dependency jars
17+
* required for a given main jar's class hierarchy. It returns a structured
18+
* DependencyResult object that encapsulates the necessary jars, classes, and reasons.
2319
*/
2420
public class DependencyAnalyzer {
2521
private final Path mainJar;
2622
private final Path librariesDir;
2723

2824
// Maps className to the jar that hosts it (for library jars)
2925
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<>();
26+
// Cache of previously computed hierarchies
27+
private final Map<String, DependencyClassHierarchy> cache = new HashMap<>();
28+
29+
// For reporting: map from jar -> map of className -> list of reasons
30+
private final Map<Path, Map<String, List<String>>> dependencyReport = new LinkedHashMap<>();
3231

3332
public DependencyAnalyzer(Path mainJar, Path librariesDir) {
3433
this.mainJar = mainJar;
3534
this.librariesDir = librariesDir;
3635
}
3736

3837
/**
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.
38+
* Analyze the main jar’s classes, build hierarchies, and determine required library jars.
39+
* Returns a structured DependencyResult object.
4140
*/
42-
public Set<Path> analyze() throws IOException {
41+
public DependencyResult analyze() throws IOException {
4342
// Step 1: Index library jars
4443
indexLibraries();
4544

46-
// Step 2: Get all classes from main jar
45+
// Step 2: Load all classes from main jar
4746
Set<String> mainClasses = loadClassesFromJar(mainJar);
4847

4948
// Step 3: Resolve hierarchical dependencies
50-
Set<Path> requiredJars = new HashSet<>();
49+
Set<Path> requiredJars = new LinkedHashSet<>();
5150
for (String cls : mainClasses) {
52-
resolveHierarchy(cls, requiredJars, mainJar, new HashSet<>());
51+
// top-level classes from main jar have a general reason
52+
resolveHierarchy(cls, requiredJars, mainJar, new HashSet<>(), "top-level class from main jar");
5353
}
54-
return requiredJars;
54+
55+
return buildResult(requiredJars);
5556
}
5657

5758
/**
58-
* Recursively resolves the hierarchy of a given class, adding necessary jars as discovered.
59+
* Recursively resolves the class hierarchy for a given class, updating requiredJars
60+
* and the dependency report as external classes are found.
61+
*
62+
* @param className The class to resolve
63+
* @param requiredJars Set of jars already identified as required
64+
* @param sourceJar The jar in which we expect to find this class
65+
* @param visited Set of visited classes to avoid cycles
66+
* @param reason A textual reason describing why we are resolving this class
5967
*/
60-
private void resolveHierarchy(String className, Set<Path> requiredJars, Path sourceJar, Set<String> visited) throws IOException {
68+
private void resolveHierarchy(String className,
69+
Set<Path> requiredJars,
70+
Path sourceJar,
71+
Set<String> visited,
72+
String reason) throws IOException {
6173
if (visited.contains(className)) return;
6274
visited.add(className);
6375

64-
DependencyClassHierarchy hierarchy = loadClassHierarchy(className, sourceJar);
76+
DependencyClassHierarchy hierarchy = loadDependencyClassHierarchy(className, sourceJar);
6577

66-
// If we found a class from a library jar
78+
// If class is from a library jar, record the reason
6779
if (!hierarchy.isMainJarClass && hierarchy.sourceJar != null) {
6880
requiredJars.add(hierarchy.sourceJar);
81+
addToReport(hierarchy.sourceJar, hierarchy.className, reason);
6982
}
7083

7184
// Resolve superclass
@@ -75,7 +88,8 @@ private void resolveHierarchy(String className, Set<Path> requiredJars, Path sou
7588
jarForSuper = classToLibraryMap.get(hierarchy.superName);
7689
}
7790
if (jarForSuper != null) {
78-
resolveHierarchy(hierarchy.superName, requiredJars, jarForSuper, visited);
91+
String superReason = "needed as superclass of " + className;
92+
resolveHierarchy(hierarchy.superName, requiredJars, jarForSuper, visited, superReason);
7993
}
8094
}
8195

@@ -86,18 +100,37 @@ private void resolveHierarchy(String className, Set<Path> requiredJars, Path sou
86100
jarForIface = classToLibraryMap.get(iface);
87101
}
88102
if (jarForIface != null) {
89-
resolveHierarchy(iface, requiredJars, jarForIface, visited);
103+
String ifaceReason = "needed as an interface of " + className;
104+
resolveHierarchy(iface, requiredJars, jarForIface, visited, ifaceReason);
90105
}
91106
}
92107
}
93108

94109
/**
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.
110+
* Construct the final DependencyResult object from the gathered report data.
97111
*/
98-
private DependencyClassHierarchy loadClassHierarchy(String className, Path presumedJar) throws IOException {
99-
if (classHierarchyCache.containsKey(className)) {
100-
return classHierarchyCache.get(className);
112+
private DependencyResult buildResult(Set<Path> requiredJars) {
113+
List<DependencyResult.JarDependency> jarDependencies = new ArrayList<>();
114+
for (Path jarPath : requiredJars) {
115+
Map<String, List<String>> classesMap = dependencyReport.getOrDefault(jarPath, Collections.emptyMap());
116+
List<DependencyResult.ClassDependency> classDependencies = new ArrayList<>();
117+
118+
for (Map.Entry<String, List<String>> entry : classesMap.entrySet()) {
119+
classDependencies.add(new DependencyResult.ClassDependency(entry.getKey(), entry.getValue()));
120+
}
121+
122+
jarDependencies.add(new DependencyResult.JarDependency(jarPath, classDependencies));
123+
}
124+
125+
return new DependencyResult(jarDependencies);
126+
}
127+
128+
/**
129+
* Load the class hierarchy of a given class. If cached, use the cache.
130+
*/
131+
private DependencyClassHierarchy loadDependencyClassHierarchy(String className, Path presumedJar) throws IOException {
132+
if (cache.containsKey(className)) {
133+
return cache.get(className);
101134
}
102135

103136
boolean fromMainJar = false;
@@ -109,38 +142,39 @@ private DependencyClassHierarchy loadClassHierarchy(String className, Path presu
109142
} else {
110143
Path libJar = classToLibraryMap.get(className);
111144
if (libJar == null) {
112-
// Not found in known jars
145+
// Not found anywhere
113146
DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null);
114-
classHierarchyCache.put(className, notFound);
147+
cache.put(className, notFound);
115148
return notFound;
116149
}
117150
classStream = getClassStream(libJar, className);
118151
jarSource = libJar;
119152
}
120153

121154
if (classStream == null) {
155+
// Not found anywhere
122156
DependencyClassHierarchy notFound = new DependencyClassHierarchy(className, null, new String[0], true, null);
123-
classHierarchyCache.put(className, notFound);
157+
cache.put(className, notFound);
124158
return notFound;
125159
}
126160

127161
ClassReader cr = new ClassReader(classStream);
128162
HierarchyVisitor visitor = new HierarchyVisitor();
129-
cr.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE);
163+
cr.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
130164

131165
DependencyClassHierarchy hierarchy = new DependencyClassHierarchy(
132-
className,
133-
visitor.superName,
134-
visitor.interfaces,
135-
fromMainJar,
136-
fromMainJar ? null : jarSource
166+
className,
167+
visitor.superName,
168+
visitor.interfaces,
169+
fromMainJar,
170+
fromMainJar ? null : jarSource
137171
);
138-
classHierarchyCache.put(className, hierarchy);
172+
cache.put(className, hierarchy);
139173
return hierarchy;
140174
}
141175

142176
/**
143-
* Index all library jars found in librariesDir by their contained classes.
177+
* Index all library jars by their classes.
144178
*/
145179
private void indexLibraries() throws IOException {
146180
try (DirectoryStream<Path> stream = Files.newDirectoryStream(librariesDir, "*.jar")) {
@@ -178,10 +212,9 @@ private Set<String> loadClassesFromJar(Path jarPath) throws IOException {
178212
}
179213

180214
/**
181-
* Retrieve an InputStream for a specified class from a given jar.
215+
* Get the InputStream of the specified class from the given jar.
182216
*/
183217
private InputStream getClassStream(Path jar, String className) throws IOException {
184-
// Need a fresh stream for each read attempt
185218
JarFile jf = new JarFile(jar.toFile());
186219
String path = className.replace('.', '/') + ".class";
187220
JarEntry entry = jf.getJarEntry(path);
@@ -193,7 +226,17 @@ private InputStream getClassStream(Path jar, String className) throws IOExceptio
193226
}
194227

195228
/**
196-
* A wrapper that closes the JarFile once the InputStream is closed.
229+
* Add an entry to the dependency report for the given jar and class.
230+
*/
231+
private void addToReport(Path jar, String className, String reason) {
232+
dependencyReport.computeIfAbsent(jar, k -> new LinkedHashMap<>());
233+
Map<String, List<String>> classes = dependencyReport.get(jar);
234+
classes.computeIfAbsent(className, k -> new ArrayList<>());
235+
classes.get(className).add(reason);
236+
}
237+
238+
/**
239+
* A wrapper that ensures the JarFile is closed when the InputStream is closed.
197240
*/
198241
private static class ClosableInputStreamWrapper extends InputStream {
199242
private final JarFile jarFile;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dev.skidfuscator.dependanalysis;
2+
3+
import java.nio.file.Path;
4+
import java.util.List;
5+
6+
/**
7+
* DependencyResult provides a structured representation of the required dependencies.
8+
* Each JarDependency represents an external jar that is needed, containing the classes
9+
* from that jar that are required and reasons for their necessity.
10+
*/
11+
public class DependencyResult {
12+
private final List<JarDependency> jarDependencies;
13+
14+
public DependencyResult(List<JarDependency> jarDependencies) {
15+
this.jarDependencies = jarDependencies;
16+
}
17+
18+
public List<JarDependency> getJarDependencies() {
19+
return jarDependencies;
20+
}
21+
22+
public void printReport() {
23+
System.out.println("Required Dependencies:");
24+
System.out.println("=====================\n");
25+
26+
if (this.getJarDependencies().isEmpty()) {
27+
System.out.println("No external dependencies are required.");
28+
return;
29+
}
30+
31+
for (DependencyResult.JarDependency jarDependency : this.getJarDependencies()) {
32+
System.out.println("JAR: " + jarDependency.getJarPath().getFileName());
33+
System.out.println("---------------------------------------------------");
34+
for (DependencyResult.ClassDependency classDep : jarDependency.getClassesNeeded()) {
35+
System.out.println(" Class: " + classDep.getClassName());
36+
for (String reason : classDep.getReasons()) {
37+
System.out.println(" - " + reason);
38+
}
39+
}
40+
System.out.println();
41+
}
42+
}
43+
public static class JarDependency {
44+
private final Path jarPath;
45+
private final List<ClassDependency> classesNeeded;
46+
47+
public JarDependency(Path jarPath, List<ClassDependency> classesNeeded) {
48+
this.jarPath = jarPath;
49+
this.classesNeeded = classesNeeded;
50+
}
51+
52+
public Path getJarPath() {
53+
return jarPath;
54+
}
55+
56+
public List<ClassDependency> getClassesNeeded() {
57+
return classesNeeded;
58+
}
59+
}
60+
61+
public static class ClassDependency {
62+
private final String className;
63+
private final List<String> reasons;
64+
65+
public ClassDependency(String className, List<String> reasons) {
66+
this.className = className;
67+
this.reasons = reasons;
68+
}
69+
70+
public String getClassName() {
71+
return className;
72+
}
73+
74+
public List<String> getReasons() {
75+
return reasons;
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)