1
- package dev . skidfuscator . dependanalysis ;
1
+ package com . example . dependencyanalyzer ;
2
2
3
+ import dev .skidfuscator .dependanalysis .DependencyClassHierarchy ;
4
+ import dev .skidfuscator .dependanalysis .DependencyResult ;
3
5
import dev .skidfuscator .dependanalysis .visitor .HierarchyVisitor ;
4
6
import org .objectweb .asm .ClassReader ;
5
7
11
13
import java .util .jar .JarFile ;
12
14
13
15
/**
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.
23
19
*/
24
20
public class DependencyAnalyzer {
25
21
private final Path mainJar ;
26
22
private final Path librariesDir ;
27
23
28
24
// Maps className to the jar that hosts it (for library jars)
29
25
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 <>();
32
31
33
32
public DependencyAnalyzer (Path mainJar , Path librariesDir ) {
34
33
this .mainJar = mainJar ;
35
34
this .librariesDir = librariesDir ;
36
35
}
37
36
38
37
/**
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 .
41
40
*/
42
- public Set < Path > analyze () throws IOException {
41
+ public DependencyResult analyze () throws IOException {
43
42
// Step 1: Index library jars
44
43
indexLibraries ();
45
44
46
- // Step 2: Get all classes from main jar
45
+ // Step 2: Load all classes from main jar
47
46
Set <String > mainClasses = loadClassesFromJar (mainJar );
48
47
49
48
// Step 3: Resolve hierarchical dependencies
50
- Set <Path > requiredJars = new HashSet <>();
49
+ Set <Path > requiredJars = new LinkedHashSet <>();
51
50
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" );
53
53
}
54
- return requiredJars ;
54
+
55
+ return buildResult (requiredJars );
55
56
}
56
57
57
58
/**
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
59
67
*/
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 {
61
73
if (visited .contains (className )) return ;
62
74
visited .add (className );
63
75
64
- DependencyClassHierarchy hierarchy = loadClassHierarchy (className , sourceJar );
76
+ DependencyClassHierarchy hierarchy = loadDependencyClassHierarchy (className , sourceJar );
65
77
66
- // If we found a class from a library jar
78
+ // If class is from a library jar, record the reason
67
79
if (!hierarchy .isMainJarClass && hierarchy .sourceJar != null ) {
68
80
requiredJars .add (hierarchy .sourceJar );
81
+ addToReport (hierarchy .sourceJar , hierarchy .className , reason );
69
82
}
70
83
71
84
// Resolve superclass
@@ -75,7 +88,8 @@ private void resolveHierarchy(String className, Set<Path> requiredJars, Path sou
75
88
jarForSuper = classToLibraryMap .get (hierarchy .superName );
76
89
}
77
90
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 );
79
93
}
80
94
}
81
95
@@ -86,18 +100,37 @@ private void resolveHierarchy(String className, Set<Path> requiredJars, Path sou
86
100
jarForIface = classToLibraryMap .get (iface );
87
101
}
88
102
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 );
90
105
}
91
106
}
92
107
}
93
108
94
109
/**
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.
97
111
*/
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 );
101
134
}
102
135
103
136
boolean fromMainJar = false ;
@@ -109,38 +142,39 @@ private DependencyClassHierarchy loadClassHierarchy(String className, Path presu
109
142
} else {
110
143
Path libJar = classToLibraryMap .get (className );
111
144
if (libJar == null ) {
112
- // Not found in known jars
145
+ // Not found anywhere
113
146
DependencyClassHierarchy notFound = new DependencyClassHierarchy (className , null , new String [0 ], true , null );
114
- classHierarchyCache .put (className , notFound );
147
+ cache .put (className , notFound );
115
148
return notFound ;
116
149
}
117
150
classStream = getClassStream (libJar , className );
118
151
jarSource = libJar ;
119
152
}
120
153
121
154
if (classStream == null ) {
155
+ // Not found anywhere
122
156
DependencyClassHierarchy notFound = new DependencyClassHierarchy (className , null , new String [0 ], true , null );
123
- classHierarchyCache .put (className , notFound );
157
+ cache .put (className , notFound );
124
158
return notFound ;
125
159
}
126
160
127
161
ClassReader cr = new ClassReader (classStream );
128
162
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 );
130
164
131
165
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
137
171
);
138
- classHierarchyCache .put (className , hierarchy );
172
+ cache .put (className , hierarchy );
139
173
return hierarchy ;
140
174
}
141
175
142
176
/**
143
- * Index all library jars found in librariesDir by their contained classes.
177
+ * Index all library jars by their classes.
144
178
*/
145
179
private void indexLibraries () throws IOException {
146
180
try (DirectoryStream <Path > stream = Files .newDirectoryStream (librariesDir , "*.jar" )) {
@@ -178,10 +212,9 @@ private Set<String> loadClassesFromJar(Path jarPath) throws IOException {
178
212
}
179
213
180
214
/**
181
- * Retrieve an InputStream for a specified class from a given jar.
215
+ * Get the InputStream of the specified class from the given jar.
182
216
*/
183
217
private InputStream getClassStream (Path jar , String className ) throws IOException {
184
- // Need a fresh stream for each read attempt
185
218
JarFile jf = new JarFile (jar .toFile ());
186
219
String path = className .replace ('.' , '/' ) + ".class" ;
187
220
JarEntry entry = jf .getJarEntry (path );
@@ -193,7 +226,17 @@ private InputStream getClassStream(Path jar, String className) throws IOExceptio
193
226
}
194
227
195
228
/**
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.
197
240
*/
198
241
private static class ClosableInputStreamWrapper extends InputStream {
199
242
private final JarFile jarFile ;
0 commit comments