-
Notifications
You must be signed in to change notification settings - Fork 147
Expand file tree
/
Copy pathCompilerUtils.java
More file actions
355 lines (329 loc) · 13.3 KB
/
Copy pathCompilerUtils.java
File metadata and controls
355 lines (329 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*
* Copyright 2013-2026 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.compiler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Unsafe;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
/**
* Provides static utility methods for runtime Java compilation, dynamic class loading,
* and class-path manipulation. Acts as the primary entry point for simple compilation tasks.
*/
public enum CompilerUtils {
; // none
/**
* Indicates whether the JVM started with debugging enabled.
*/
public static final boolean DEBUGGING = isDebug();
/**
* In-memory singleton compiler reused across calls.
*/
public static final CachedCompiler CACHED_COMPILER = new CachedCompiler(null, null);
private static final Logger LOGGER = LoggerFactory.getLogger(CompilerUtils.class);
private static final Method DEFINE_CLASS_METHOD;
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String JAVA_CLASS_PATH = "java.class.path";
static JavaCompiler s_compiler;
static StandardJavaFileManager s_standardJavaFileManager;
/*
* Use sun.misc.Unsafe to gain access to ClassLoader.defineClass. This allows
* compiled bytecode to be defined without standard reflection checks. The
* fallback path calls setAccessible if the internal 'override' field is absent.
*/
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe u = (Unsafe) theUnsafe.get(null);
DEFINE_CLASS_METHOD = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
try {
Field f = AccessibleObject.class.getDeclaredField("override");
long offset = u.objectFieldOffset(f);
u.putBoolean(DEFINE_CLASS_METHOD, offset, true);
} catch (NoSuchFieldException e) {
DEFINE_CLASS_METHOD.setAccessible(true);
}
} catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException e) {
throw new AssertionError(e);
}
}
static {
reset();
}
private static boolean isDebug() {
String inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments().toString();
return inputArguments.contains("-Xdebug") || inputArguments.contains("-agentlib:jdwp=");
}
/**
* Reinitialises the cached {@link JavaCompiler}. This method is not thread-safe
* and callers must serialise access if used outside static initialisation.
*
* @throws AssertionError if the compiler classes cannot be loaded.
*/
private static void reset() {
s_compiler = ToolProvider.getSystemJavaCompiler();
if (s_compiler == null) {
try {
Class<?> javacTool = Class.forName("com.sun.tools.javac.api.JavacTool");
Method create = javacTool.getMethod("create");
s_compiler = (JavaCompiler) create.invoke(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}
/**
* Loads a class from a source file found on the class-path or local file system.
* Thread-safe as it delegates to the cached compiler.
*
* @param className expected class name of the outer class.
* @param resourceName full file name with extension.
* @return the loaded outer class.
* @throws IOException if the resource cannot be read.
* @throws ClassNotFoundException if the compiled class name differs or fails to initialise.
*/
public static Class<?> loadFromResource(@NotNull String className, @NotNull String resourceName) throws IOException, ClassNotFoundException {
return loadFromJava(className, readText(resourceName));
}
/**
* Load a java class from text.
*
* @param className expected class name of the outer class.
* @param javaCode to compile and load.
* @return the outer class loaded.
* @throws ClassNotFoundException the class name didn't match or failed to initialise.
*/
private static Class<?> loadFromJava(@NotNull String className, @NotNull String javaCode) throws ClassNotFoundException {
return CACHED_COMPILER.loadFromJava(Thread.currentThread().getContextClassLoader(), className, javaCode);
}
/**
* Adds a directory to the compilation class-path. This method is not thread-safe
* and should be called in a single-threaded context.
*
* @param dir directory to add.
* @return {@code true} if the directory exists and was appended.
* @throws AssertionError if the compiler cannot be reinitialised.
*/
public static boolean addClassPath(@NotNull String dir) {
File file = new File(dir);
if (file.exists()) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException ignored) {
path = file.getAbsolutePath();
}
if (!Arrays.asList(System.getProperty(JAVA_CLASS_PATH).split(File.pathSeparator)).contains(path))
System.setProperty(JAVA_CLASS_PATH, System.getProperty(JAVA_CLASS_PATH) + File.pathSeparator + path);
} else {
return false;
}
reset();
return true;
}
/**
* Normalizes relative paths and rejects traversal attempts beyond the current root.
*
* @param path value to sanitize.
* @return normalized path when safe.
* @throws IllegalArgumentException if the path attempts to traverse upward ("..").
*/
static Path sanitizePath(@NotNull Path path) {
Objects.requireNonNull(path, "path");
Path normalized = path.normalize();
if (normalized.isAbsolute()) {
return normalized;
}
if (normalized.getNameCount() > 0 && "..".equals(normalized.getName(0).toString())) {
throw new IllegalArgumentException("Path traversal attempt: " + path);
}
return normalized;
}
/**
* Define a class for byte code.
*
* @param className expected to load.
* @param bytes of the byte code.
*/
public static void defineClass(@NotNull String className, @NotNull byte[] bytes) {
defineClass(Thread.currentThread().getContextClassLoader(), className, bytes);
}
/**
* Defines a class from the supplied bytecode.
* Thread-safe and uses {@code Unsafe} to bypass access checks.
*
* @param classLoader class loader to define the class within.
* @param className expected binary name.
* @param bytes compiled bytecode for the class.
* @return the defined class instance.
* @throws AssertionError if {@code defineClass} cannot be invoked.
*/
public static Class<?> defineClass(@Nullable ClassLoader classLoader, @NotNull String className, @NotNull byte[] bytes) {
try {
return (Class) DEFINE_CLASS_METHOD.invoke(classLoader, className, bytes, 0, bytes.length);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
//noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
throw new AssertionError(e.getCause());
}
}
/**
* Reads the supplied resource as UTF-8 text. Thread-safe.
*
* @param resourceName resource path or inline text prefixed with '='.
* @return the text contents of the resource.
* @throws IOException if an I/O error occurs while reading.
*/
private static String readText(@NotNull String resourceName) throws IOException {
if (resourceName.startsWith("="))
return resourceName.substring(1);
StringWriter sw = new StringWriter();
Reader isr = new InputStreamReader(getInputStream(resourceName), UTF_8);
try {
char[] chars = new char[8 * 1024];
int len;
while ((len = isr.read(chars)) > 0)
sw.write(chars, 0, len);
} finally {
close(isr);
}
return sw.toString();
}
@NotNull
private static String decodeUTF8(@NotNull byte[] bytes) {
try {
return new String(bytes, UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
@Nullable
@SuppressWarnings("ReturnOfNull")
private static byte[] readBytes(@NotNull File file) {
if (!file.exists()) return null;
long len = file.length();
if (len > Runtime.getRuntime().totalMemory() / 10)
throw new IllegalStateException("Attempted to read large file " + file + " was " + len + " bytes.");
byte[] bytes = new byte[(int) len];
DataInputStream dis = null;
try {
dis = new DataInputStream(new FileInputStream(file));
dis.readFully(bytes);
} catch (IOException e) {
close(dis);
LOGGER.warn("Unable to read {}", file, e);
throw new IllegalStateException("Unable to read file " + file, e);
}
return bytes;
}
private static void close(@Nullable Closeable closeable) {
if (closeable != null)
try {
closeable.close();
} catch (IOException e) {
LOGGER.trace("Failed to close {}", closeable, e);
}
}
/**
* Writes the provided text to the target file using UTF-8.
* Not thread-safe and may create or overwrite the file.
*
* @param file destination file.
* @param text text to write.
* @return {@code true} if the contents changed.
* @throws IllegalStateException if the file cannot be written.
*/
public static boolean writeText(@NotNull File file, @NotNull String text) {
return writeBytes(file, encodeUTF8(text));
}
/**
* Encodes the given text as UTF-8 bytes.
*
* @param text value to encode.
* @return UTF-8 encoded representation.
* @throws AssertionError if the JVM does not support UTF-8.
*/
@NotNull
private static byte[] encodeUTF8(@NotNull String text) {
try {
return text.getBytes(UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
/**
* Writes the given bytes to the specified file. Not thread-safe.
*
* @param file destination file.
* @param bytes bytes to write.
* @return {@code true} if the file contents were updated.
* @throws IllegalStateException if the write fails.
*/
public static boolean writeBytes(@NotNull File file, @NotNull byte[] bytes) {
File parentDir = file.getParentFile();
if (!parentDir.isDirectory() && !parentDir.mkdirs())
throw new IllegalStateException("Unable to create directory " + parentDir);
// only write to disk if it has changed.
File bak = null;
if (file.exists()) {
byte[] bytes2 = readBytes(file);
if (Arrays.equals(bytes, bytes2))
return false;
bak = new File(parentDir, file.getName() + ".bak");
file.renameTo(bak);
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
fos.write(bytes);
} catch (IOException e) {
close(fos);
LOGGER.warn("Unable to write {} as {}", file, decodeUTF8(bytes), e);
file.delete();
if (bak != null)
bak.renameTo(file);
throw new IllegalStateException("Unable to write " + file, e);
} finally {
if (bak != null && bak.exists() && file.exists()) {
if (!bak.delete()) {
LOGGER.debug("Unable to delete backup {}", bak);
}
}
}
return true;
}
/**
* Opens the named resource as an {@link InputStream}. Thread-safe.
*
* @param filename name of a class-path resource or file; a leading '=' denotes inline text.
* @return stream for the resource.
* @throws FileNotFoundException if no file or resource exists.
*/
@NotNull
private static InputStream getInputStream(@NotNull String filename) throws FileNotFoundException {
if (filename.isEmpty()) throw new IllegalArgumentException("The file name cannot be empty.");
if (filename.charAt(0) == '=') return new ByteArrayInputStream(encodeUTF8(filename.substring(1)));
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
InputStream is = contextClassLoader.getResourceAsStream(filename);
if (is != null) return is;
InputStream is2 = contextClassLoader.getResourceAsStream('/' + filename);
if (is2 != null) return is2;
return new FileInputStream(filename);
}
}