Skip to content

Commit f4fcfc4

Browse files
committed
Release 0.3.12
1 parent 6ae7f7c commit f4fcfc4

7 files changed

Lines changed: 435 additions & 56 deletions

File tree

pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929

3030
<properties>
3131
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
32-
<revision>0.3.11</revision>
32+
<revision>0.3.12</revision>
3333
<maven.compiler.release>17</maven.compiler.release>
3434
<picocli.version>4.7.7</picocli.version>
3535
<javaparser.version>3.27.1</javaparser.version>
3636
<bouncycastle.version>1.78.1</bouncycastle.version>
3737
<junit.version>5.11.4</junit.version>
3838
<maven.invoker.version>3.3.0</maven.invoker.version>
3939
<gradle.tooling.api.version>7.3-20210825160000+0000</gradle.tooling.api.version>
40+
<cfr.version>0.152</cfr.version>
4041
<maven.compiler.plugin.version>3.14.0</maven.compiler.plugin.version>
4142
<maven.surefire.plugin.version>3.5.2</maven.surefire.plugin.version>
4243
<maven.jar.plugin.version>3.4.2</maven.jar.plugin.version>
@@ -76,6 +77,12 @@
7677
<version>${gradle.tooling.api.version}</version>
7778
</dependency>
7879

80+
<dependency>
81+
<groupId>org.benf</groupId>
82+
<artifactId>cfr</artifactId>
83+
<version>${cfr.version}</version>
84+
</dependency>
85+
7986
<dependency>
8087
<groupId>org.junit.jupiter</groupId>
8188
<artifactId>junit-jupiter</artifactId>

src/main/java/com/jaipilot/cli/JaiPilotVersionProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static String resolveVersion() {
1313
Package commandPackage = JaiPilotCli.class.getPackage();
1414
String version = commandPackage == null ? null : commandPackage.getImplementationVersion();
1515
if (version == null || version.isBlank()) {
16-
version = "0.3.7";
16+
version = "0.3.12";
1717
}
1818
return version;
1919
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package com.jaipilot.cli.classpath;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.ArrayList;
8+
import java.util.Collection;
9+
import java.util.Comparator;
10+
import java.util.LinkedHashSet;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Optional;
14+
import java.util.Set;
15+
import java.util.zip.ZipEntry;
16+
import java.util.zip.ZipFile;
17+
import org.benf.cfr.reader.api.CfrDriver;
18+
import org.benf.cfr.reader.api.OutputSinkFactory;
19+
import org.benf.cfr.reader.api.SinkReturns;
20+
21+
final class CfrDecompiler {
22+
23+
private static final System.Logger LOGGER = System.getLogger(CfrDecompiler.class.getName());
24+
25+
Optional<String> decompile(Path classContainer, String classEntryPath) {
26+
if (classContainer == null || classEntryPath == null || classEntryPath.isBlank()) {
27+
return Optional.empty();
28+
}
29+
30+
Path normalizedContainer = classContainer.toAbsolutePath().normalize();
31+
if (Files.isDirectory(normalizedContainer)) {
32+
Path classFile = normalizedContainer.resolve(classEntryPath).toAbsolutePath().normalize();
33+
if (!Files.isRegularFile(classFile)) {
34+
return Optional.empty();
35+
}
36+
return decompileClassFile(classFile);
37+
}
38+
39+
String fileName = normalizedContainer.getFileName() == null
40+
? ""
41+
: normalizedContainer.getFileName().toString();
42+
if (fileName.endsWith(".jar")) {
43+
return decompileFromJar(normalizedContainer, classEntryPath);
44+
}
45+
if (fileName.endsWith(".class") && Files.isRegularFile(normalizedContainer)) {
46+
return decompileClassFile(normalizedContainer);
47+
}
48+
return Optional.empty();
49+
}
50+
51+
private Optional<String> decompileFromJar(Path jarPath, String classEntryPath) {
52+
Path tempDirectory = null;
53+
try {
54+
tempDirectory = Files.createTempDirectory("jaipilot-cfr-");
55+
ExtractedClass extractedClass = extractClassFromJar(jarPath, classEntryPath, tempDirectory).orElse(null);
56+
if (extractedClass == null) {
57+
return Optional.empty();
58+
}
59+
return decompileClassFile(extractedClass.classFilePath());
60+
} catch (IOException exception) {
61+
LOGGER.log(System.Logger.Level.DEBUG, "cfr extraction failed for {0}: {1}", jarPath, exception.getMessage());
62+
return Optional.empty();
63+
} finally {
64+
deleteRecursively(tempDirectory);
65+
}
66+
}
67+
68+
private Optional<ExtractedClass> extractClassFromJar(Path jarPath, String classEntryPath, Path tempDirectory) throws IOException {
69+
String normalizedEntryPath = normalizeEntryPath(classEntryPath);
70+
if (normalizedEntryPath.isBlank()) {
71+
return Optional.empty();
72+
}
73+
74+
String outerEntryPath = outerClassEntryPath(normalizedEntryPath);
75+
try (ZipFile zipFile = new ZipFile(jarPath.toFile())) {
76+
ZipEntry primaryEntry = findPrimaryEntry(zipFile, normalizedEntryPath, outerEntryPath);
77+
if (primaryEntry == null) {
78+
return Optional.empty();
79+
}
80+
81+
String outerPrefix = outerEntryPath.substring(0, outerEntryPath.length() - ".class".length());
82+
Set<String> entriesToExtract = new LinkedHashSet<>();
83+
entriesToExtract.add(primaryEntry.getName());
84+
entriesToExtract.add(outerEntryPath);
85+
for (ZipEntry entry : List.copyOf(zipFile.stream().toList())) {
86+
if (entry.isDirectory()) {
87+
continue;
88+
}
89+
String name = entry.getName();
90+
if (name.startsWith(outerPrefix + "$") && name.endsWith(".class")) {
91+
entriesToExtract.add(name);
92+
}
93+
}
94+
95+
Path primaryPath = null;
96+
for (String entryName : entriesToExtract) {
97+
ZipEntry entry = zipFile.getEntry(entryName);
98+
if (entry == null || entry.isDirectory()) {
99+
continue;
100+
}
101+
Path extractedPath = extractEntry(zipFile, entry, tempDirectory);
102+
if (entry.getName().equals(primaryEntry.getName())) {
103+
primaryPath = extractedPath;
104+
}
105+
}
106+
return Optional.ofNullable(primaryPath).map(ExtractedClass::new);
107+
}
108+
}
109+
110+
private ZipEntry findPrimaryEntry(ZipFile zipFile, String normalizedEntryPath, String outerEntryPath) {
111+
ZipEntry outer = zipFile.getEntry(outerEntryPath);
112+
if (outer != null && !outer.isDirectory()) {
113+
return outer;
114+
}
115+
ZipEntry requested = zipFile.getEntry(normalizedEntryPath);
116+
if (requested != null && !requested.isDirectory()) {
117+
return requested;
118+
}
119+
return null;
120+
}
121+
122+
private Path extractEntry(ZipFile zipFile, ZipEntry entry, Path outputRoot) throws IOException {
123+
Path outputPath = outputRoot.resolve(entry.getName()).toAbsolutePath().normalize();
124+
Path parent = outputPath.getParent();
125+
if (parent != null) {
126+
Files.createDirectories(parent);
127+
}
128+
try (InputStream inputStream = zipFile.getInputStream(entry)) {
129+
Files.copy(inputStream, outputPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
130+
}
131+
return outputPath;
132+
}
133+
134+
private Optional<String> decompileClassFile(Path classFilePath) {
135+
List<String> decompiledSource = new ArrayList<>();
136+
List<String> exceptions = new ArrayList<>();
137+
138+
try {
139+
CfrDriver cfrDriver = new CfrDriver.Builder()
140+
.withOptions(Map.of(
141+
"showversion", "false",
142+
"silent", "true"
143+
))
144+
.withOutputSink(new CfrOutputSink(decompiledSource, exceptions))
145+
.build();
146+
147+
cfrDriver.analyse(List.of(classFilePath.toString()));
148+
} catch (Exception exception) {
149+
LOGGER.log(System.Logger.Level.DEBUG, "cfr decompilation failed for {0}: {1}", classFilePath, exception.getMessage());
150+
return Optional.empty();
151+
}
152+
153+
if (decompiledSource.isEmpty()) {
154+
if (!exceptions.isEmpty()) {
155+
LOGGER.log(System.Logger.Level.DEBUG, "cfr did not produce Java source for {0}: {1}", classFilePath, exceptions.get(0));
156+
}
157+
return Optional.empty();
158+
}
159+
160+
return decompiledSource.stream()
161+
.filter(source -> source != null && !source.isBlank())
162+
.findFirst()
163+
.map(this::normalizeLineEndings);
164+
}
165+
166+
private String normalizeEntryPath(String classEntryPath) {
167+
String normalized = classEntryPath.replace('\\', '/');
168+
while (normalized.startsWith("/")) {
169+
normalized = normalized.substring(1);
170+
}
171+
return normalized;
172+
}
173+
174+
private String outerClassEntryPath(String classEntryPath) {
175+
int firstDollar = classEntryPath.indexOf('$');
176+
if (firstDollar < 0) {
177+
return classEntryPath;
178+
}
179+
int classSuffix = classEntryPath.lastIndexOf(".class");
180+
if (classSuffix < 0) {
181+
return classEntryPath.substring(0, firstDollar);
182+
}
183+
return classEntryPath.substring(0, firstDollar) + ".class";
184+
}
185+
186+
private String normalizeLineEndings(String source) {
187+
return source.replace("\r\n", "\n").replace('\r', '\n');
188+
}
189+
190+
private void deleteRecursively(Path path) {
191+
if (path == null || !Files.exists(path)) {
192+
return;
193+
}
194+
try (var paths = Files.walk(path)) {
195+
paths.sorted(Comparator.reverseOrder()).forEach(candidate -> {
196+
try {
197+
Files.deleteIfExists(candidate);
198+
} catch (IOException ignored) {
199+
// Best-effort cleanup for temporary decompiler files.
200+
}
201+
});
202+
} catch (IOException ignored) {
203+
// Best-effort cleanup for temporary decompiler files.
204+
}
205+
}
206+
207+
private record ExtractedClass(Path classFilePath) {
208+
}
209+
210+
private static final class CfrOutputSink implements OutputSinkFactory {
211+
212+
private final List<String> decompiledSource;
213+
private final List<String> exceptions;
214+
215+
CfrOutputSink(List<String> decompiledSource, List<String> exceptions) {
216+
this.decompiledSource = decompiledSource;
217+
this.exceptions = exceptions;
218+
}
219+
220+
@Override
221+
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> available) {
222+
if (sinkType == SinkType.JAVA) {
223+
if (available.contains(SinkClass.DECOMPILED)) {
224+
return List.of(SinkClass.DECOMPILED);
225+
}
226+
if (available.contains(SinkClass.STRING)) {
227+
return List.of(SinkClass.STRING);
228+
}
229+
return List.of();
230+
}
231+
232+
if (sinkType == SinkType.EXCEPTION && available.contains(SinkClass.EXCEPTION_MESSAGE)) {
233+
return List.of(SinkClass.EXCEPTION_MESSAGE);
234+
}
235+
if (sinkType == SinkType.EXCEPTION && available.contains(SinkClass.STRING)) {
236+
return List.of(SinkClass.STRING);
237+
}
238+
return List.of();
239+
}
240+
241+
@Override
242+
@SuppressWarnings("unchecked")
243+
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
244+
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) {
245+
return sinkable -> decompiledSource.add(((SinkReturns.Decompiled) sinkable).getJava());
246+
}
247+
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.STRING) {
248+
return sinkable -> decompiledSource.add(String.valueOf(sinkable));
249+
}
250+
if (sinkType == SinkType.EXCEPTION && sinkClass == SinkClass.EXCEPTION_MESSAGE) {
251+
return sinkable -> exceptions.add(((SinkReturns.ExceptionMessage) sinkable).getMessage());
252+
}
253+
if (sinkType == SinkType.EXCEPTION && sinkClass == SinkClass.STRING) {
254+
return sinkable -> exceptions.add(String.valueOf(sinkable));
255+
}
256+
return sinkable -> {
257+
};
258+
}
259+
}
260+
}

0 commit comments

Comments
 (0)