-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/master'
- Loading branch information
Showing
5 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
src/main/java/org/kettingpowered/ketting/common/deobf/ClassMapping.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.kettingpowered.ketting.common.deobf; | ||
|
||
import java.util.Map; | ||
|
||
public record ClassMapping( | ||
String obfName, | ||
String deobfName, | ||
Map<String, String> methods | ||
) {} |
12 changes: 12 additions & 0 deletions
12
src/main/java/org/kettingpowered/ketting/common/deobf/Mapping.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.kettingpowered.ketting.common.deobf; | ||
|
||
public record Mapping(String obfMethod, String obfDesc, String mojang) { | ||
|
||
static Mapping from(String obfMethod, String obfDesc, String mojang) { | ||
return new Mapping(obfMethod, obfDesc, mojang); | ||
} | ||
|
||
public String obf() { | ||
return obfMethod + obfDesc; | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
src/main/java/org/kettingpowered/ketting/common/deobf/MappingLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package org.kettingpowered.ketting.common.deobf; | ||
|
||
import net.fabricmc.mappingio.MappingReader; | ||
import net.fabricmc.mappingio.format.MappingFormat; | ||
import net.fabricmc.mappingio.tree.MappingTree; | ||
import net.fabricmc.mappingio.tree.MemoryMappingTree; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.io.*; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.*; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
|
||
public class MappingLoader { | ||
|
||
public final Map<String, ClassMapping> mappings; | ||
public final Map<String, List<Mapping>> bukkitMappings = new HashMap<>(); | ||
|
||
MappingLoader(File mojmap, Map<String, String> bukkitMethods) throws IOException { | ||
mappings = loadMappingsIfPresent(mojmap); | ||
bukkitMethods.forEach(this::parseBukkitMappings); | ||
} | ||
|
||
private void parseBukkitMappings(String obf, String mojang) { | ||
String raw = obf.substring(0, obf.indexOf('(') - 1); | ||
int lastSlash = raw.lastIndexOf('/'); | ||
String classPath = raw.substring(0, lastSlash); | ||
String methodName = raw.substring(lastSlash + 1); | ||
String desc = obf.substring(obf.indexOf(methodName) + methodName.length() + 1).trim(); | ||
bukkitMappings.computeIfAbsent(classPath.replace('/', '.'), k -> new ArrayList<>()).add(Mapping.from(methodName, desc, mojang)); | ||
} | ||
|
||
private @Nullable Map<String, ClassMapping> loadMappingsIfPresent(File mojmap) throws IOException { | ||
try (final @Nullable InputStream mappingsInputStream = new FileInputStream(mojmap)) { | ||
final MemoryMappingTree tree = new MemoryMappingTree(); | ||
MappingReader.read(new InputStreamReader(mappingsInputStream, StandardCharsets.UTF_8), MappingFormat.PROGUARD_FILE, tree); | ||
|
||
String LEFT = "named"; | ||
String RIGHT = "official"; | ||
|
||
// Since the mapping works like 'named -> official', the source is technically the right side | ||
tree.setSrcNamespace(RIGHT); | ||
tree.setDstNamespaces(Collections.singletonList(LEFT)); | ||
|
||
final Set<ClassMapping> classes = new HashSet<>(); | ||
|
||
final StringPool pool = new StringPool(); | ||
for (final MappingTree.ClassMapping cls : tree.getClasses()) { | ||
final Map<String, String> methods = new HashMap<>(); | ||
|
||
for (final MappingTree.MethodMapping methodMapping : cls.getMethods()) { | ||
methods.put( | ||
pool.string(methodKey( | ||
methodMapping.getName(LEFT), | ||
methodMapping.getDesc(LEFT) | ||
)), | ||
pool.string(methodMapping.getName(RIGHT)) | ||
); | ||
} | ||
|
||
final ClassMapping map = new ClassMapping( | ||
cls.getName(LEFT).replace('/', '.'), | ||
cls.getName(RIGHT).replace('/', '.'), | ||
Map.copyOf(methods) | ||
); | ||
classes.add(map); | ||
} | ||
|
||
return Set.copyOf(classes).stream().collect(Collectors.toUnmodifiableMap(ClassMapping::deobfName, map -> map)); | ||
} | ||
} | ||
|
||
public String methodKey(final String obfName, final String obfDescriptor) { | ||
return obfName + obfDescriptor; | ||
} | ||
|
||
public String getFromMojang(String className, String mojangName, String desc) { | ||
if (mappings == null || mappings.isEmpty() || mojangName == null || mojangName.isBlank()) | ||
return mojangName; | ||
|
||
ClassMapping mapping = mappings.get(className); | ||
if (mapping == null) | ||
return mojangName; | ||
|
||
return mapping.methods().getOrDefault(methodKey(mojangName, desc), mojangName); | ||
} | ||
|
||
private static final class StringPool { | ||
private final Map<String, String> pool = new HashMap<>(); | ||
|
||
public String string(final String string) { | ||
return this.pool.computeIfAbsent(string, Function.identity()); | ||
} | ||
} | ||
} |
184 changes: 184 additions & 0 deletions
184
src/main/java/org/kettingpowered/ketting/common/deobf/StacktraceDeobfuscator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package org.kettingpowered.ketting.common.deobf; | ||
|
||
import it.unimi.dsi.fastutil.ints.IntArrayList; | ||
import it.unimi.dsi.fastutil.ints.IntList; | ||
import org.jetbrains.annotations.Nullable; | ||
import org.objectweb.asm.*; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.*; | ||
import java.util.*; | ||
|
||
public class StacktraceDeobfuscator { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(StacktraceDeobfuscator.class); | ||
private static MappingLoader mappingLoader; | ||
private static ClassLoader serverClassLoader; | ||
|
||
private static boolean enabled; | ||
|
||
private static final Map<Class<?>, Map<String, IntList>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) { | ||
@Override | ||
protected boolean removeEldestEntry(final Map.Entry<Class<?>, Map<String, IntList>> eldest) { | ||
return this.size() > 127; | ||
} | ||
}); | ||
|
||
public static void init(File mojmap, Map<String, String> bukkitMethods, ClassLoader serverClassLoader) { | ||
if (mappingLoader != null) return; | ||
|
||
try { | ||
mappingLoader = new MappingLoader(mojmap, bukkitMethods); | ||
} catch (IOException e) { | ||
LOGGER.error("Failed to load mappings", e); | ||
return; | ||
} | ||
|
||
StacktraceDeobfuscator.serverClassLoader = serverClassLoader; | ||
|
||
int mojmapSize = mappingLoader.mappings.size(); | ||
int bukkitSize = mappingLoader.bukkitMappings.values().stream().mapToInt(List::size).sum(); | ||
LOGGER.info("Loaded {} mappings [{} mojmap, {} bukkit]", mojmapSize + bukkitSize, mojmapSize, bukkitSize); | ||
enabled = true; | ||
} | ||
|
||
public static void setEnabled(boolean enabled) { | ||
StacktraceDeobfuscator.enabled = enabled; | ||
} | ||
|
||
public static void deobf(Throwable throwable) { | ||
if (!enabled || mappingLoader == null) return; | ||
|
||
throwable.setStackTrace(deobf(throwable.getStackTrace())); | ||
|
||
final Throwable cause = throwable.getCause(); | ||
if (cause != null) deobf(cause); | ||
|
||
Arrays.stream(throwable.getSuppressed()).forEach(StacktraceDeobfuscator::deobf); | ||
} | ||
|
||
public static StackTraceElement[] deobf(StackTraceElement[] stacktrace) { | ||
if (!enabled || mappingLoader == null || stacktrace.length == 0) return stacktrace; | ||
|
||
final StackTraceElement[] deobf = new StackTraceElement[stacktrace.length]; | ||
for (int i = 0; i < stacktrace.length; i++) { | ||
final StackTraceElement element = stacktrace[i]; | ||
final String className = element.getClassName(); | ||
|
||
final List<Mapping> classMappings = mappingLoader.bukkitMappings.get(className); | ||
if (classMappings == null) { | ||
deobf[i] = element; | ||
continue; | ||
} | ||
|
||
final Class<?> clazz; | ||
try { | ||
clazz = Class.forName(className, true, serverClassLoader); | ||
} catch (ClassNotFoundException e) { | ||
LOGGER.error("Failed to find class for {}", className, e); | ||
deobf[i] = element; | ||
continue; | ||
} | ||
|
||
final String methodKey = determineMethodForLine(clazz, element.getLineNumber()); | ||
|
||
if (methodKey == null) { | ||
deobf[i] = element; | ||
continue; | ||
} | ||
|
||
final Mapping map = classMappings.stream().filter(m -> m.obf().equals(methodKey)).findFirst().orElse(null); | ||
|
||
if (map == null) { | ||
deobf[i] = element; | ||
continue; | ||
} | ||
|
||
final String methodName = mappingLoader.getFromMojang(className, map.mojang(), map.obfDesc()); | ||
|
||
deobf[i] = new StackTraceElement( | ||
element.getClassLoaderName(), | ||
element.getModuleName(), | ||
element.getModuleVersion(), | ||
className, | ||
Objects.equals(methodName, map.mojang()) ? element.getMethodName() : methodName, | ||
sourceFileName(className), | ||
element.getLineNumber() | ||
); | ||
} | ||
return deobf; | ||
} | ||
|
||
private static @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) { | ||
final Map<String, IntList> lineMap = lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap); | ||
for (final var entry : lineMap.entrySet()) { | ||
final String methodKey = entry.getKey(); | ||
final IntList lines = entry.getValue(); | ||
for (int i = 0, linesSize = lines.size(); i < linesSize; i++) { | ||
final int num = lines.getInt(i); | ||
if (num == lineNumber) { | ||
return methodKey; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
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 Map<String, IntList> buildLineMap(final Class<?> key) { | ||
final Map<String, IntList> lineMap = new HashMap<>(); | ||
final class LineCollectingMethodVisitor extends MethodVisitor { | ||
private final IntList lines = new IntArrayList(); | ||
private final String name; | ||
private final String descriptor; | ||
|
||
LineCollectingMethodVisitor(String name, String descriptor) { | ||
super(Opcodes.ASM9); | ||
this.name = name; | ||
this.descriptor = descriptor; | ||
} | ||
|
||
@Override | ||
public void visitLineNumber(int line, Label start) { | ||
super.visitLineNumber(line, start); | ||
this.lines.add(line); | ||
} | ||
|
||
@Override | ||
public void visitEnd() { | ||
super.visitEnd(); | ||
lineMap.put(mappingLoader.methodKey(this.name, this.descriptor), this.lines); | ||
} | ||
} | ||
final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) { | ||
@Override | ||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { | ||
return new LineCollectingMethodVisitor(name, descriptor); | ||
} | ||
}; | ||
try { | ||
final @Nullable InputStream inputStream = serverClassLoader.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; | ||
} | ||
} |