diff options
author | Jonas Herzig <me@johni0702.de> | 2019-08-30 16:48:00 +0200 |
---|---|---|
committer | Jonas Herzig <me@johni0702.de> | 2019-08-30 16:48:00 +0200 |
commit | cfdc125366b756a7d164502ebcde22e2976c9319 (patch) | |
tree | 03559547dd1cc52ec9a3f2a13c490da3d02182e6 /src/main/kotlin/com/replaymod/gradle/remap | |
parent | 06279d658496a3fb0a1909bad1ebdb9a60a37aed (diff) | |
download | Remap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.gz Remap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.bz2 Remap-cfdc125366b756a7d164502ebcde22e2976c9319.zip |
Convert implementation and build script to Kotlin
Diffstat (limited to 'src/main/kotlin/com/replaymod/gradle/remap')
7 files changed, 736 insertions, 0 deletions
diff --git a/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt b/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt new file mode 100644 index 0000000..cd04918 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt @@ -0,0 +1,11 @@ +package com.replaymod.gradle.remap + +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.model.ClassMapping +import org.cadixdev.lorenz.model.FieldMapping +import org.cadixdev.lorenz.model.MethodMapping + +fun MappingSet.findClassMapping(obfuscatedName: String): ClassMapping<*, *>? = getClassMapping(obfuscatedName).orElse(null) +fun ClassMapping<*, *>.findFieldMapping(obfuscatedName: String): FieldMapping? = getFieldMapping(obfuscatedName).orElse(null) +fun ClassMapping<*, *>.findMethodMapping(signature: MethodSignature): MethodMapping? = getMethodMapping(signature).orElse(null) diff --git a/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt b/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt new file mode 100644 index 0000000..95ee858 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt @@ -0,0 +1,365 @@ +package com.replaymod.gradle.remap + +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.* +import com.replaymod.gradle.remap.PsiUtils.getSignature +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.model.ClassMapping +import java.util.* + +internal class PsiMapper(private val map: MappingSet, private val file: PsiFile) { + private val mixinMappings = mutableMapOf<String, ClassMapping<*, *>>() + private var error: Boolean = false + private val changes = TreeMap<TextRange, String>(Comparator.comparing<TextRange, Int> { it.startOffset }) + + private fun error(at: PsiElement, message: String) { + val line = StringUtil.offsetToLineNumber(file.text, at.textOffset) + System.err.println(file.name + ":" + line + ": " + message) + error = true + } + + private fun replace(e: PsiElement, with: String) { + changes[e.textRange] = with + } + + private fun replaceIdentifier(parent: PsiElement, with: String) { + for (child in parent.children) { + if (child is PsiIdentifier) { + replace(child, with) + return + } + } + } + + private fun valid(e: PsiElement): Boolean { + val range = e.textRange + val before = changes.ceilingKey(range) + return before == null || !before.intersects(range) + } + + private fun getResult(text: String): String? { + if (error) { + return null + } + var result = text + for ((key, value) in changes.descendingMap()) { + result = key.replace(result, value) + } + return result + } + + private fun map(expr: PsiElement, field: PsiField) { + val fieldName = field.name ?: return + val declaringClass = field.containingClass ?: return + val name = declaringClass.qualifiedName ?: return + var mapping: ClassMapping<*, *>? = this.mixinMappings[name] + if (mapping == null) { + mapping = map.findClassMapping(name) + } + if (mapping == null) return + val mapped = mapping.findFieldMapping(fieldName)?.deobfuscatedName + if (mapped == null || mapped == fieldName) return + replaceIdentifier(expr, mapped) + + if (expr is PsiJavaCodeReferenceElement + && !expr.isQualified // qualified access is fine + && !isSwitchCase(expr) // referencing constants in case statements is fine + ) { + error(expr, "Implicit member reference to remapped field \"$fieldName\". " + + "This can cause issues if the remapped reference becomes shadowed by a local variable and is therefore forbidden. " + + "Use \"this.$fieldName\" instead.") + } + } + + private fun map(expr: PsiElement, method: PsiMethod) { + if (method.isConstructor) return + + var declaringClass: PsiClass? = method.containingClass ?: return + val parentQueue = ArrayDeque<PsiClass>() + parentQueue.offer(declaringClass) + var mapping: ClassMapping<*, *>? = null + + var name = declaringClass!!.qualifiedName + if (name != null) { + mapping = mixinMappings[name] + } + while (true) { + if (mapping != null) { + val mapped = mapping.findMethodMapping(getSignature(method))?.deobfuscatedName + if (mapped != null) { + if (mapped != method.name) { + replaceIdentifier(expr, mapped) + } + return + } + mapping = null + } + while (mapping == null) { + declaringClass = parentQueue.poll() + if (declaringClass == null) return + + val superClass = declaringClass.superClass + if (superClass != null) { + parentQueue.offer(superClass) + } + for (anInterface in declaringClass.interfaces) { + parentQueue.offer(anInterface) + } + + name = declaringClass.qualifiedName + if (name == null) continue + mapping = map.findClassMapping(name) + } + } + } + + private fun map(expr: PsiElement, resolved: PsiQualifiedNamedElement) { + val name = resolved.qualifiedName ?: return + val mapping = map.findClassMapping(name) ?: return + var mapped = mapping.deobfuscatedName + if (mapped == name) return + mapped = mapped.replace('/', '.') + + if (expr.text == name) { + replace(expr, mapped) + return + } + replaceIdentifier(expr, mapped.substring(mapped.lastIndexOf('.') + 1)) + } + + private fun map(expr: PsiElement, resolved: PsiElement?) { + when (resolved) { + is PsiField -> map(expr, resolved) + is PsiMethod -> map(expr, resolved) + is PsiClass, is PsiPackage -> map(expr, resolved as PsiQualifiedNamedElement) + } + } + + // Note: Supports only Mixins with a single target (ignores others) and only ones specified via class literals + private fun getMixinTarget(annotation: PsiAnnotation): PsiClass? { + for (pair in annotation.parameterList.attributes) { + val name = pair.name + if (name != null && "value" != name) continue + val value = pair.value + if (value !is PsiClassObjectAccessExpression) continue + val type = value.operand + val reference = type.innermostComponentReferenceElement ?: continue + return reference.resolve() as PsiClass? + } + return null + } + + private fun remapAccessors(mapping: ClassMapping<*, *>) { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val annotation = method.getAnnotation(CLASS_ACCESSOR) ?: return + + val methodName = method.name + val targetByName = when { + methodName.startsWith("is") -> methodName.substring(2) + methodName.startsWith("get") || methodName.startsWith("set") -> methodName.substring(3) + else -> null + }?.decapitalize() + + val target = annotation.parameterList.attributes.find { + it.name == null || it.name == "value" + }?.literalValue ?: targetByName ?: throw IllegalArgumentException("Cannot determine accessor target for $method") + + val mapped = mapping.findFieldMapping(target)?.deobfuscatedName + if (mapped != null && mapped != target) { + // Update accessor target + replace(annotation.parameterList, if (mapped == targetByName) { + // Mapped name matches implied target, can just remove the explict target + "" + } else { + // Mapped name does not match implied target, need to set the target as annotation value + "(\"" + StringUtil.escapeStringCharacters(mapped) + "\")" + }) + } + } + }) + } + + private fun remapInjectsAndRedirects(mapping: ClassMapping<*, *>) { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitMethod(method: PsiMethod) { + val annotation = method.getAnnotation(CLASS_INJECT) ?: method.getAnnotation(CLASS_REDIRECT) ?: return + + for (attribute in annotation.parameterList.attributes) { + if ("method" != attribute.name) continue + // Note: mixin supports multiple targets, we do not (yet) + val literalValue = attribute.literalValue ?: continue + var mapped: String? + if (literalValue.contains("(")) { + mapped = mapping.findMethodMapping(MethodSignature.of(literalValue))?.deobfuscatedName + } else { + mapped = null + for (methodMapping in mapping.methodMappings) { + if (methodMapping.obfuscatedName == literalValue) { + val name = methodMapping.deobfuscatedName + if (mapped != null && mapped != name) { + error(attribute, "Ambiguous mixin method \"$literalValue\" maps to \"$mapped\" and \"$name\"") + } + mapped = name + } + } + } + if (mapped != null && mapped != literalValue) { + val value = attribute.value!! + replace(value, '"'.toString() + mapped + '"'.toString()) + } + } + } + }) + } + + private fun remapInternalType(internalType: String, result: StringBuilder): ClassMapping<*, *>? { + if (internalType[0] == 'L') { + val type = internalType.substring(1, internalType.length - 1).replace('/', '.') + val mapping = map.findClassMapping(type) + if (mapping != null) { + result.append('L').append(mapping.fullDeobfuscatedName).append(';') + return mapping + } + } + result.append(internalType) + return null + } + + private fun remapFullyQualifiedMethodOrField(signature: String): String { + val ownerEnd = signature.indexOf(';') + var argsBegin = signature.indexOf('(') + var argsEnd = signature.indexOf(')') + val method = argsBegin != -1 + if (!method) { + argsEnd = signature.indexOf(':') + argsBegin = argsEnd + } + val owner = signature.substring(0, ownerEnd + 1) + val name = signature.substring(ownerEnd + 1, argsBegin) + val returnType = signature.substring(argsEnd + 1) + + val builder = StringBuilder(signature.length + 32) + val mapping = remapInternalType(owner, builder) + var mapped: String? = null + if (mapping != null) { + mapped = (if (method) { + mapping.findMethodMapping(MethodSignature.of(signature.substring(ownerEnd + 1))) + } else { + mapping.findFieldMapping(name) + })?.deobfuscatedName + } + builder.append(mapped ?: name) + if (method) { + builder.append('(') + val args = signature.substring(argsBegin + 1, argsEnd) + var i = 0 + while (i < args.length) { + val c = args[i] + if (c != 'L') { + builder.append(c) + i++ + continue + } + val end = args.indexOf(';', i) + val arg = args.substring(i, end + 1) + remapInternalType(arg, builder) + i = end + i++ + } + builder.append(')') + } else { + builder.append(':') + } + remapInternalType(returnType, builder) + return builder.toString() + } + + private fun remapAtTargets() { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (CLASS_AT != annotation.qualifiedName) { + super.visitAnnotation(annotation) + return + } + + for (attribute in annotation.parameterList.attributes) { + if ("target" != attribute.name) continue + val signature = attribute.literalValue ?: continue + val newSignature = remapFullyQualifiedMethodOrField(signature) + if (newSignature != signature) { + val value = attribute.value!! + replace(value, "\"$newSignature\"") + } + } + } + }) + } + + fun remapFile(): String? { + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitClass(psiClass: PsiClass) { + val annotation = psiClass.getAnnotation(CLASS_MIXIN) ?: return + + remapAtTargets() + + val target = getMixinTarget(annotation) ?: return + val qualifiedName = target.qualifiedName ?: return + + val mapping = map.findClassMapping(qualifiedName) ?: return + + mixinMappings[psiClass.qualifiedName!!] = mapping + + if (!mapping.fieldMappings.isEmpty()) { + remapAccessors(mapping) + } + if (!mapping.methodMappings.isEmpty()) { + remapInjectsAndRedirects(mapping) + } + } + }) + + file.accept(object : JavaRecursiveElementVisitor() { + override fun visitField(field: PsiField) { + if (valid(field)) { + map(field, field) + } + super.visitField(field) + } + + override fun visitMethod(method: PsiMethod) { + if (valid(method)) { + map(method, method) + } + super.visitMethod(method) + } + + override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) { + if (valid(reference)) { + map(reference, reference.resolve()) + } + super.visitReferenceElement(reference) + } + }) + + return getResult(file.text) + } + + companion object { + private const val CLASS_MIXIN = "org.spongepowered.asm.mixin.Mixin" + private const val CLASS_ACCESSOR = "org.spongepowered.asm.mixin.gen.Accessor" + private const val CLASS_AT = "org.spongepowered.asm.mixin.injection.At" + private const val CLASS_INJECT = "org.spongepowered.asm.mixin.injection.Inject" + private const val CLASS_REDIRECT = "org.spongepowered.asm.mixin.injection.Redirect" + + private fun isSwitchCase(e: PsiElement): Boolean { + if (e is PsiSwitchLabelStatement) { + return true + } + val parent = e.parent + return parent != null && isSwitchCase(parent) + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt b/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt new file mode 100644 index 0000000..de29956 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt @@ -0,0 +1,41 @@ +package com.replaymod.gradle.remap + +import com.intellij.psi.* +import com.intellij.psi.util.TypeConversionUtil +import org.cadixdev.bombe.type.ArrayType +import org.cadixdev.bombe.type.FieldType +import org.cadixdev.bombe.type.MethodDescriptor +import org.cadixdev.bombe.type.ObjectType +import org.cadixdev.bombe.type.Type +import org.cadixdev.bombe.type.VoidType +import org.cadixdev.bombe.type.signature.MethodSignature + +internal object PsiUtils { + fun getSignature(method: PsiMethod): MethodSignature = MethodSignature(method.name, getDescriptor(method)) + + private fun getDescriptor(method: PsiMethod): MethodDescriptor = MethodDescriptor( + method.parameterList.parameters.map { getFieldType(it.type) }, + getType(method.returnType) + ) + + private fun getFieldType(type: PsiType?): FieldType = when (val erasedType = TypeConversionUtil.erasure(type)) { + is PsiPrimitiveType -> FieldType.of(erasedType.kind.binaryName) + is PsiArrayType -> { + val array = erasedType as PsiArrayType? + ArrayType(array!!.arrayDimensions, getFieldType(array.deepComponentType)) + } + is PsiClassType -> { + val resolved = erasedType.resolve() ?: throw NullPointerException("Failed to resolve type $erasedType") + val qualifiedName = resolved.qualifiedName + ?: throw NullPointerException("Type $erasedType has no qualified name.") + ObjectType(qualifiedName) + } + else -> throw IllegalArgumentException("Cannot translate type " + erasedType!!) + } + + private fun getType(type: PsiType?): Type = if (TypeConversionUtil.isVoidType(type)) { + VoidType.INSTANCE + } else { + getFieldType(type) + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt new file mode 100644 index 0000000..303ee42 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt @@ -0,0 +1,149 @@ +package com.replaymod.gradle.remap + +import com.intellij.codeInsight.CustomExceptionHandler +import com.intellij.mock.MockProject +import com.intellij.openapi.extensions.ExtensionPoint +import com.intellij.openapi.extensions.Extensions +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.local.CoreLocalFileSystem +import com.intellij.psi.PsiManager +import com.intellij.psi.search.GlobalSearchScope +import com.replaymod.gradle.remap.legacy.LegacyMapping +import org.cadixdev.lorenz.MappingSet +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.config.ContentRoot +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.StandardOpenOption +import java.util.* +import kotlin.system.exitProcess + +class Transformer(private val map: MappingSet) { + var classpath: Array<String>? = null + private var fail: Boolean = false + + @Throws(IOException::class) + fun remap(sources: Map<String, String>): Map<String, String> { + val tmpDir = Files.createTempDirectory("remap") + try { + for ((unitName, source) in sources) { + val path = tmpDir.resolve(unitName) + Files.createDirectories(path.parent) + Files.write(path, source.toByteArray(StandardCharsets.UTF_8), StandardOpenOption.CREATE) + } + + val config = CompilerConfiguration() + config.put(CommonConfigurationKeys.MODULE_NAME, "main") + config.add<ContentRoot>(CLIConfigurationKeys.CONTENT_ROOTS, JavaSourceRoot(tmpDir.toFile(), "")) + config.add<ContentRoot>(CLIConfigurationKeys.CONTENT_ROOTS, KotlinSourceRoot(tmpDir.toAbsolutePath().toString(), false)) + config.put<MessageCollector>(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, PrintingMessageCollector(System.err, MessageRenderer.GRADLE_STYLE, true)) + + val environment = KotlinCoreEnvironment.createForProduction( + Disposer.newDisposable(), + config, + EnvironmentConfigFiles.JVM_CONFIG_FILES + ) + val rootArea = Extensions.getRootArea() + if (!rootArea.hasExtensionPoint(CustomExceptionHandler.KEY)) { + rootArea.registerExtensionPoint(CustomExceptionHandler.KEY.name, CustomExceptionHandler::class.java.name, ExtensionPoint.Kind.INTERFACE) + } + + val project = environment.project as MockProject + + environment.updateClasspath(classpath!!.map { JvmClasspathRoot(File(it)) }) + + TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( + project, + emptyList(), + NoScopeRecordCliBindingTrace(), + environment.configuration, + { scope: GlobalSearchScope -> environment.createPackagePartProvider(scope) } + ) + + val vfs = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) as CoreLocalFileSystem + val results = HashMap<String, String>() + for (name in sources.keys) { + val file = vfs.findFileByIoFile(tmpDir.resolve(name).toFile())!! + val psiFile = PsiManager.getInstance(project).findFile(file)!! + + val mapped = PsiMapper(map, psiFile).remapFile() + if (mapped == null) { + fail = true + continue + } + results[name] = mapped + } + return results + } finally { + Files.walk(tmpDir).map<File> { it.toFile() }.sorted(Comparator.reverseOrder()).forEach { it.delete() } + } + } + + companion object { + + @Throws(IOException::class) + @JvmStatic + fun main(args: Array<String>) { + val mappings: MappingSet = if (args[0].isEmpty()) { + MappingSet.create() + } else { + LegacyMapping.readMappingSet(File(args[0]).toPath(), args[1] == "true") + } + val transformer = Transformer(mappings) + + val reader = BufferedReader(InputStreamReader(System.`in`)) + + transformer.classpath = (1..Integer.parseInt(args[2])).map { reader.readLine() }.toTypedArray() + + val sources = mutableMapOf<String, String>() + while (true) { + val name = reader.readLine() + if (name == null || name.isEmpty()) { + break + } + + val lines = arrayOfNulls<String>(Integer.parseInt(reader.readLine())) + for (i in lines.indices) { + lines[i] = reader.readLine() + } + val source = lines.joinToString("\n") + + sources[name] = source + } + + val results = transformer.remap(sources) + + for (name in sources.keys) { + println(name) + val lines = results.getValue(name).split("\n").dropLastWhile { it.isEmpty() }.toTypedArray() + println(lines.size) + for (line in lines) { + println(line) + } + } + + if (transformer.fail) { + exitProcess(1) + } + } + } + +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt new file mode 100644 index 0000000..389b1c9 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt @@ -0,0 +1,109 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.lorenz.MappingSet + +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.util.HashMap + +class LegacyMapping(var oldName: String, var newName: String) { + var fields: MutableMap<String, String> = mutableMapOf() + var methods: MutableMap<String, String> = mutableMapOf() + + companion object { + @Throws(IOException::class) + fun readMappingSet(mappingFile: Path, invert: Boolean): MappingSet { + return LegacyMappingsReader(readMappings(mappingFile, invert)).read() + } + + @Throws(IOException::class) + fun readMappings(mappingFile: Path, invert: Boolean): Map<String, LegacyMapping> { + val mappings = HashMap<String, LegacyMapping>() + val revMappings = HashMap<String, LegacyMapping>() + var lineNumber = 0 + for (line in Files.readAllLines(mappingFile, StandardCharsets.UTF_8)) { + lineNumber++ + if (line.trim { it <= ' ' }.startsWith("#") || line.trim { it <= ' ' }.isEmpty()) continue + + val parts = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + require(!(parts.size < 2 || line.contains(";"))) { "Failed to parse line $lineNumber in $mappingFile." } + + var mapping: LegacyMapping? = mappings[parts[0]] + if (mapping == null) { + mapping = LegacyMapping(parts[0], parts[0]) + mappings[mapping.oldName] = mapping + } + + if (parts.size == 2) { + // Class mapping + mapping.newName = parts[1] + // Possibly merge with reverse mapping + val revMapping = revMappings.remove(mapping.newName) + if (revMapping != null) { + mapping.fields.putAll(revMapping.fields) + mapping.methods.putAll(revMapping.methods) + } + revMappings[mapping.newName] = mapping + } else if (parts.size == 3 || parts.size == 4) { + var fromName = parts[1] + var toName: String + var revMapping: LegacyMapping? + if (parts.size == 4) { + toName = parts[3] + revMapping = revMappings[parts[2]] + if (revMapping == null) { + revMapping = LegacyMapping(parts[2], parts[2]) + revMappings[revMapping.newName] = revMapping + } + } else { + toName = parts[2] + revMapping = mapping + } + if (fromName.endsWith("()")) { + // Method mapping + fromName = fromName.substring(0, fromName.length - 2) + toName = toName.substring(0, toName.length - 2) + mapping.methods[fromName] = toName + revMapping.methods[fromName] = toName + } else { + // Field mapping + mapping.fields[fromName] = toName + revMapping.fields[fromName] = toName + } + } else { + throw IllegalArgumentException("Failed to parse line $lineNumber in $mappingFile.") + } + } + if (invert) { + + (mappings.values + revMappings.values).distinct().forEach { mapping -> + mapping.oldName = mapping.newName.also { mapping.newName = mapping.oldName } + mapping.fields = mapping.fields.map { it.value to it.key }.toMap(mutableMapOf()) + mapping.methods = mapping.methods.map { it.value to it.key }.toMap(mutableMapOf()) + } + } + val result = mutableMapOf<String, LegacyMapping>() + for (mapping in (mappings.values + revMappings.values)) { + val key = mapping.oldName + val other = result[key] + result[key] = if (other != null) { + if (other.oldName != other.newName) { + require(mapping.oldName == mapping.newName || other.oldName == mapping.oldName || other.newName == mapping.newName) { + "Conflicting mappings: ${mapping.oldName} -> ${mapping.newName} and ${other.oldName} -> ${other.newName}" + } + mapping.oldName = other.oldName + mapping.newName = other.newName + } + mapping.fields.putAll(other.fields) + mapping.methods.putAll(other.methods) + mapping + } else { + mapping + } + } + return result + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt new file mode 100644 index 0000000..7737200 --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt @@ -0,0 +1,33 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.bombe.type.signature.MethodSignature +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.impl.MappingSetModelFactoryImpl +import org.cadixdev.lorenz.impl.model.TopLevelClassMappingImpl +import org.cadixdev.lorenz.model.MethodMapping +import org.cadixdev.lorenz.model.TopLevelClassMapping + +import java.util.Optional + +class LegacyMappingSetModelFactory : MappingSetModelFactoryImpl() { + override fun createTopLevelClassMapping(parent: MappingSet, obfuscatedName: String, deobfuscatedName: String): TopLevelClassMapping { + return object : TopLevelClassMappingImpl(parent, obfuscatedName, deobfuscatedName) { + private fun stripDesc(signature: MethodSignature): MethodSignature { + // actual descriptor isn't included in legacy format + return MethodSignature.of(signature.name, "()V") + } + + override fun hasMethodMapping(signature: MethodSignature): Boolean { + return super.hasMethodMapping(signature) || super.hasMethodMapping(stripDesc(signature)) + } + + override fun getMethodMapping(signature: MethodSignature): Optional<MethodMapping> { + var maybeMapping = super.getMethodMapping(signature) + if (!maybeMapping.isPresent || !maybeMapping.get().hasMappings()) { + maybeMapping = super.getMethodMapping(stripDesc(signature)) + } + return maybeMapping + } + } + } +} diff --git a/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt new file mode 100644 index 0000000..8a6144e --- /dev/null +++ b/src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt @@ -0,0 +1,28 @@ +package com.replaymod.gradle.remap.legacy + +import org.cadixdev.lorenz.MappingSet +import org.cadixdev.lorenz.io.MappingsReader + +class LegacyMappingsReader(private val map: Map<String, LegacyMapping>) : MappingsReader() { + + override fun read(): MappingSet { + return read(MappingSet.create(LegacyMappingSetModelFactory())) + } + + override fun read(mappings: MappingSet): MappingSet { + require(mappings.modelFactory is LegacyMappingSetModelFactory) { "legacy mappings must use legacy model factory, use read() instead" } + for (legacyMapping in map.values) { + val classMapping = mappings.getOrCreateClassMapping(legacyMapping.oldName) + .setDeobfuscatedName(legacyMapping.newName) + for ((key, value) in legacyMapping.fields) { + classMapping.getOrCreateFieldMapping(key).deobfuscatedName = value + } + for ((key, value) in legacyMapping.methods) { + classMapping.getOrCreateMethodMapping(key, "()V").deobfuscatedName = value + } + } + return mappings + } + + override fun close() {} +} |