Deobfuscate stacktraces in log messages, crash reports, and etc.
This commit is contained in:
156
paper-server/src/main/java/io/papermc/paper/util/ObfHelper.java
Normal file
156
paper-server/src/main/java/io/papermc/paper/util/ObfHelper.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package io.papermc.paper.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import net.neoforged.srgutils.IMappingFile;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public enum ObfHelper {
|
||||
INSTANCE;
|
||||
|
||||
private final @Nullable Map<String, ClassMapping> mappingsByObfName;
|
||||
private final @Nullable Map<String, ClassMapping> mappingsByMojangName;
|
||||
|
||||
ObfHelper() {
|
||||
final @Nullable Set<ClassMapping> maps = loadMappingsIfPresent();
|
||||
if (maps != null) {
|
||||
this.mappingsByObfName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::obfName, map -> map));
|
||||
this.mappingsByMojangName = maps.stream().collect(Collectors.toUnmodifiableMap(ClassMapping::mojangName, map -> map));
|
||||
} else {
|
||||
this.mappingsByObfName = null;
|
||||
this.mappingsByMojangName = null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Map<String, ClassMapping> mappingsByObfName() {
|
||||
return this.mappingsByObfName;
|
||||
}
|
||||
|
||||
public @Nullable Map<String, ClassMapping> mappingsByMojangName() {
|
||||
return this.mappingsByMojangName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the obf name for a given class by its Mojang name. Will
|
||||
* return the input string if mappings are not present.
|
||||
*
|
||||
* @param fullyQualifiedMojangName fully qualified class name (dotted)
|
||||
* @return mapped or original fully qualified (dotted) class name
|
||||
*/
|
||||
public String reobfClassName(final String fullyQualifiedMojangName) {
|
||||
if (this.mappingsByMojangName == null) {
|
||||
return fullyQualifiedMojangName;
|
||||
}
|
||||
|
||||
final ClassMapping map = this.mappingsByMojangName.get(fullyQualifiedMojangName);
|
||||
if (map == null) {
|
||||
return fullyQualifiedMojangName;
|
||||
}
|
||||
|
||||
return map.obfName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the Mojang name for a given class by its obf name. Will
|
||||
* return the input string if mappings are not present.
|
||||
*
|
||||
* @param fullyQualifiedObfName fully qualified class name (dotted)
|
||||
* @return mapped or original fully qualified (dotted) class name
|
||||
*/
|
||||
public String deobfClassName(final String fullyQualifiedObfName) {
|
||||
if (this.mappingsByObfName == null) {
|
||||
return fullyQualifiedObfName;
|
||||
}
|
||||
|
||||
final ClassMapping map = this.mappingsByObfName.get(fullyQualifiedObfName);
|
||||
if (map == null) {
|
||||
return fullyQualifiedObfName;
|
||||
}
|
||||
|
||||
return map.mojangName();
|
||||
}
|
||||
|
||||
private static @Nullable Set<ClassMapping> loadMappingsIfPresent() {
|
||||
try (final @Nullable InputStream mappingsInputStream = ObfHelper.class.getClassLoader().getResourceAsStream("META-INF/mappings/reobf.tiny")) {
|
||||
if (mappingsInputStream == null) {
|
||||
return null;
|
||||
}
|
||||
final IMappingFile mappings = IMappingFile.load(mappingsInputStream); // Mappings are mojang->spigot
|
||||
final Set<ClassMapping> classes = new HashSet<>();
|
||||
|
||||
final StringPool pool = new StringPool();
|
||||
for (final IMappingFile.IClass cls : mappings.getClasses()) {
|
||||
final Map<String, String> methods = new HashMap<>();
|
||||
final Map<String, String> fields = new HashMap<>();
|
||||
final Map<String, String> strippedMethods = new HashMap<>();
|
||||
|
||||
for (final IMappingFile.IMethod methodMapping : cls.getMethods()) {
|
||||
methods.put(
|
||||
pool.string(methodKey(
|
||||
Objects.requireNonNull(methodMapping.getMapped()),
|
||||
Objects.requireNonNull(methodMapping.getMappedDescriptor())
|
||||
)),
|
||||
pool.string(Objects.requireNonNull(methodMapping.getOriginal()))
|
||||
);
|
||||
|
||||
strippedMethods.put(
|
||||
pool.string(pool.string(strippedMethodKey(
|
||||
methodMapping.getMapped(),
|
||||
methodMapping.getDescriptor()
|
||||
))),
|
||||
pool.string(methodMapping.getOriginal())
|
||||
);
|
||||
}
|
||||
for (final IMappingFile.IField field : cls.getFields()) {
|
||||
fields.put(
|
||||
pool.string(field.getMapped()),
|
||||
pool.string(field.getOriginal())
|
||||
);
|
||||
}
|
||||
|
||||
final ClassMapping map = new ClassMapping(
|
||||
Objects.requireNonNull(cls.getMapped()).replace('/', '.'),
|
||||
Objects.requireNonNull(cls.getOriginal()).replace('/', '.'),
|
||||
Map.copyOf(methods),
|
||||
Map.copyOf(fields),
|
||||
Map.copyOf(strippedMethods)
|
||||
);
|
||||
classes.add(map);
|
||||
}
|
||||
|
||||
return Set.copyOf(classes);
|
||||
} catch (final IOException ex) {
|
||||
System.err.println("Failed to load mappings.");
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String strippedMethodKey(final String methodName, final String methodDescriptor) {
|
||||
final String methodKey = methodKey(methodName, methodDescriptor);
|
||||
final int returnDescriptorEnd = methodKey.indexOf(')');
|
||||
return methodKey.substring(0, returnDescriptorEnd + 1);
|
||||
}
|
||||
|
||||
public static String methodKey(final String methodName, final String methodDescriptor) {
|
||||
return methodName + methodDescriptor;
|
||||
}
|
||||
|
||||
public record ClassMapping(
|
||||
String obfName,
|
||||
String mojangName,
|
||||
Map<String, String> methodsByObf,
|
||||
Map<String, String> fieldsByObf,
|
||||
// obf name with mapped desc to mapped name. return value is excluded from desc as reflection doesn't use it
|
||||
Map<String, String> strippedMethods
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package io.papermc.paper.util;
|
||||
|
||||
import io.papermc.paper.configuration.GlobalConfiguration;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public enum StacktraceDeobfuscator {
|
||||
INSTANCE;
|
||||
|
||||
private final Map<Class<?>, Int2ObjectMap<String>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(final Map.Entry<Class<?>, Int2ObjectMap<String>> eldest) {
|
||||
return this.size() > 127;
|
||||
}
|
||||
});
|
||||
|
||||
public void deobfuscateThrowable(final Throwable throwable) {
|
||||
if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
|
||||
return;
|
||||
}
|
||||
|
||||
throwable.setStackTrace(this.deobfuscateStacktrace(throwable.getStackTrace()));
|
||||
final Throwable cause = throwable.getCause();
|
||||
if (cause != null) {
|
||||
this.deobfuscateThrowable(cause);
|
||||
}
|
||||
for (final Throwable suppressed : throwable.getSuppressed()) {
|
||||
this.deobfuscateThrowable(suppressed);
|
||||
}
|
||||
}
|
||||
|
||||
public StackTraceElement[] deobfuscateStacktrace(final StackTraceElement[] traceElements) {
|
||||
if (GlobalConfiguration.get() != null && !GlobalConfiguration.get().logging.deobfuscateStacktraces) { // handle null as true
|
||||
return traceElements;
|
||||
}
|
||||
|
||||
final @Nullable Map<String, ObfHelper.ClassMapping> mappings = ObfHelper.INSTANCE.mappingsByObfName();
|
||||
if (mappings == null || traceElements.length == 0) {
|
||||
return traceElements;
|
||||
}
|
||||
final StackTraceElement[] result = new StackTraceElement[traceElements.length];
|
||||
for (int i = 0; i < traceElements.length; i++) {
|
||||
final StackTraceElement element = traceElements[i];
|
||||
|
||||
final String className = element.getClassName();
|
||||
final String methodName = element.getMethodName();
|
||||
|
||||
final ObfHelper.ClassMapping classMapping = mappings.get(className);
|
||||
if (classMapping == null) {
|
||||
result[i] = element;
|
||||
continue;
|
||||
}
|
||||
|
||||
final Class<?> clazz;
|
||||
try {
|
||||
clazz = Class.forName(className);
|
||||
} catch (final ClassNotFoundException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
final @Nullable String methodKey = this.determineMethodForLine(clazz, element.getLineNumber());
|
||||
final @Nullable String mappedMethodName = methodKey == null ? null : classMapping.methodsByObf().get(methodKey);
|
||||
|
||||
result[i] = new StackTraceElement(
|
||||
element.getClassLoaderName(),
|
||||
element.getModuleName(),
|
||||
element.getModuleVersion(),
|
||||
classMapping.mojangName(),
|
||||
mappedMethodName != null ? mappedMethodName : methodName,
|
||||
sourceFileName(classMapping.mojangName()),
|
||||
element.getLineNumber()
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) {
|
||||
return this.lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap).get(lineNumber);
|
||||
}
|
||||
|
||||
private static String sourceFileName(final String fullClassName) {
|
||||
final int dot = fullClassName.lastIndexOf('.');
|
||||
final String className = dot == -1
|
||||
? fullClassName
|
||||
: fullClassName.substring(dot + 1);
|
||||
final String rootClassName = className.split("\\$")[0];
|
||||
return rootClassName + ".java";
|
||||
}
|
||||
|
||||
private static Int2ObjectMap<String> buildLineMap(final Class<?> key) {
|
||||
final StringPool pool = new StringPool();
|
||||
final Int2ObjectMap<String> lineMap = new Int2ObjectOpenHashMap<>();
|
||||
final class LineCollectingMethodVisitor extends MethodVisitor {
|
||||
private final String name;
|
||||
private final String descriptor;
|
||||
|
||||
LineCollectingMethodVisitor(final String name, final String descriptor) {
|
||||
super(Opcodes.ASM9);
|
||||
this.name = name;
|
||||
this.descriptor = descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitLineNumber(final int line, final Label start) {
|
||||
lineMap.put(line, pool.string(ObfHelper.methodKey(this.name, this.descriptor)));
|
||||
}
|
||||
}
|
||||
final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
|
||||
return new LineCollectingMethodVisitor(name, descriptor);
|
||||
}
|
||||
};
|
||||
try {
|
||||
final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader()
|
||||
.getResourceAsStream(key.getName().replace('.', '/') + ".class");
|
||||
if (inputStream == null) {
|
||||
throw new IllegalStateException("Could not find class file: " + key.getName());
|
||||
}
|
||||
final byte[] classData;
|
||||
try (inputStream) {
|
||||
classData = inputStream.readAllBytes();
|
||||
}
|
||||
final ClassReader reader = new ClassReader(classData);
|
||||
reader.accept(classVisitor, 0);
|
||||
} catch (final IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return lineMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.papermc.paper.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
|
||||
/**
|
||||
* De-duplicates {@link String} instances without using {@link String#intern()}.
|
||||
*
|
||||
* <p>Interning may not be desired as we may want to use the heap for our pool,
|
||||
* so it can be garbage collected as normal, etc.</p>
|
||||
*
|
||||
* <p>Additionally, interning can be slow due to the potentially large size of the
|
||||
* pool (as it is shared for the entire JVM), and because most JVMs implement
|
||||
* it using JNI.</p>
|
||||
*/
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public final class StringPool {
|
||||
private final Map<String, String> pool;
|
||||
|
||||
public StringPool() {
|
||||
this(new HashMap<>());
|
||||
}
|
||||
|
||||
public StringPool(final Map<String, String> map) {
|
||||
this.pool = map;
|
||||
}
|
||||
|
||||
public String string(final String string) {
|
||||
return this.pool.computeIfAbsent(string, Function.identity());
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ public class WatchdogThread extends Thread
|
||||
}
|
||||
log.log( Level.SEVERE, "\tStack:" );
|
||||
//
|
||||
for ( StackTraceElement stack : thread.getStackTrace() )
|
||||
for ( StackTraceElement stack : io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(thread.getStackTrace()) ) // Paper
|
||||
{
|
||||
log.log( Level.SEVERE, "\t\t" + stack );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user