aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
authorJonas Herzig <me@johni0702.de>2019-08-30 16:48:00 +0200
committerJonas Herzig <me@johni0702.de>2019-08-30 16:48:00 +0200
commitcfdc125366b756a7d164502ebcde22e2976c9319 (patch)
tree03559547dd1cc52ec9a3f2a13c490da3d02182e6 /src/main/kotlin
parent06279d658496a3fb0a1909bad1ebdb9a60a37aed (diff)
downloadRemap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.gz
Remap-cfdc125366b756a7d164502ebcde22e2976c9319.tar.bz2
Remap-cfdc125366b756a7d164502ebcde22e2976c9319.zip
Convert implementation and build script to Kotlin
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/LorenzExtensions.kt11
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/PsiMapper.kt365
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/PsiUtils.kt41
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/Transformer.kt149
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMapping.kt109
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingSetModelFactory.kt33
-rw-r--r--src/main/kotlin/com/replaymod/gradle/remap/legacy/LegacyMappingsReader.kt28
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() {}
+}