瀏覽代碼

Compiled code verify (#1080)

* Compiled code verify

* Run `verifyCompiledClasses` in `check` task

* Redesign verification

Co-authored-by: Bo Zhang <bo@gradle.com>

* Disable console verify

* Decoupling

Co-authored-by: Bo Zhang <bo@gradle.com>
Karlatemp 4 年之前
父節點
當前提交
f9353b6aef

+ 2 - 0
build.gradle.kts

@@ -69,6 +69,8 @@ tasks.register("publishMiraiCoreArtifactsToMavenLocal") {
     )
 }
 
+analyzes.CompiledCodeVerify.run { registerAllVerifyTasks() }
+
 allprojects {
     group = "net.mamoe"
     version = Versions.project

+ 2 - 7
buildSrc/src/main/kotlin/androidutil/AndroidApiLevelCheck.kt → buildSrc/src/main/kotlin/analyzes/AndroidApiLevelCheck.kt

@@ -9,7 +9,7 @@
 
 @file:Suppress("MemberVisibilityCanBePrivate")
 
-package androidutil
+package analyzes
 
 import groovy.util.Node
 import groovy.util.XmlParser
@@ -20,7 +20,6 @@ import org.objectweb.asm.tree.ClassNode
 import org.objectweb.asm.tree.FieldInsnNode
 import org.objectweb.asm.tree.MethodInsnNode
 import java.io.File
-import java.net.URL
 
 object AndroidApiLevelCheck {
     data class ClassInfo(
@@ -260,11 +259,7 @@ object AndroidApiLevelCheck {
             .filter { it.isFile && it.extension == "class" }
             .map { file ->
                 kotlin.runCatching {
-                    val cnode = ClassNode()
-                    file.inputStream().use {
-                        ClassReader(it).accept(cnode, 0)
-                    }
-                    cnode
+                    AsmUtil.run { file.readClass() }
                 }.getOrNull() to file
             }
             .filter { it.first != null }

+ 105 - 0
buildSrc/src/main/kotlin/analyzes/AsmUtil.kt

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ *  https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package analyzes
+
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.FieldNode
+import org.objectweb.asm.tree.MethodNode
+import java.io.File
+import java.io.InputStream
+
+typealias AsmClasses = Map<String, ClassNode>
+typealias AsmClassesM = MutableMap<String, ClassNode>
+
+object AsmUtil {
+    fun ClassNode.getMethod(name: String, desc: String, isStatic: Boolean): MethodNode? {
+        return methods?.firstOrNull {
+            it.name == name && it.desc == desc && ((it.access and Opcodes.ACC_STATIC) != 0) == isStatic
+        }
+    }
+
+    fun ClassNode.getField(name: String, desc: String, isStatic: Boolean): FieldNode? {
+        return fields?.firstOrNull {
+            it.name == name && it.desc == desc && ((it.access and Opcodes.ACC_STATIC) != 0) == isStatic
+        }
+    }
+
+    fun File.readClass(): ClassNode = inputStream().use { it.readClass() }
+
+    fun InputStream.readClass(): ClassNode {
+        val cnode = ClassNode()
+        ClassReader(this).accept(cnode, 0)
+        return cnode
+    }
+
+    private fun AsmClassesM.patchJvmClass(owner: String) {
+        if (owner.startsWith("java/") || owner.startsWith("javax/")) {
+            if (!this.containsKey(owner)) {
+                ClassLoader.getSystemClassLoader().getResourceAsStream("$owner.class")?.use {
+                    val c = it.readClass()
+                    this[c.name] = c
+                }
+            }
+        }
+    }
+
+    fun AsmClassesM.hasField(
+        owner: String,
+        name: String,
+        desc: String,
+        opcode: Int
+    ): Boolean {
+        patchJvmClass(owner)
+        val c = this[owner] ?: return false
+        val isStatic = opcode == Opcodes.GETSTATIC || opcode == Opcodes.PUTSTATIC
+        if (c.getField(name, desc, isStatic) != null) {
+            return true
+        }
+        if (isStatic) return false
+        return hasField(c.superName ?: "", name, desc, opcode)
+    }
+
+    fun AsmClassesM.hasMethod(
+        owner: String,
+        name: String,
+        desc: String,
+        opcode: Int
+    ): Boolean {
+        patchJvmClass(owner)
+        when (opcode) {
+            Opcodes.INVOKESTATIC -> {
+                val c = this[owner] ?: return false
+                return c.getMethod(name, desc, true) != null
+            }
+            Opcodes.INVOKEINTERFACE,
+            Opcodes.INVOKESPECIAL,
+            Opcodes.INVOKEVIRTUAL -> {
+                fun loopFind(current: String): Boolean {
+                    patchJvmClass(current)
+                    val c = this[current] ?: return false
+                    if (c.getMethod(name, desc, false) != null) return true
+                    c.superName?.let {
+                        if (loopFind(it)) {
+                            return true
+                        }
+                    }
+                    c.interfaces?.forEach {
+                        if (loopFind(it)) return true
+                    }
+                    return false
+                }
+                return loopFind(owner)
+            }
+        }
+        return false
+    }
+}

+ 90 - 0
buildSrc/src/main/kotlin/analyzes/CompiledCodeVerify.kt

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ *  https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package analyzes
+
+import org.gradle.api.Project
+import java.io.File
+
+typealias VerifyAction = (classes: Sequence<File>, libraries: Sequence<File>) -> Unit
+
+data class ProjectInfo(val isMpp: Boolean, val name: String)
+
+fun JvmProjectInfo(name: String) = ProjectInfo(false, name)
+fun MppProjectInfo(name: String) = ProjectInfo(true, name)
+
+@Suppress("MemberVisibilityCanBePrivate")
+object CompiledCodeVerify {
+
+    private const val RUN_ALL_VERITY_TASK_NAME = "runAllVerify"
+    private const val VERIFICATION_GROUP_NAME = "verification"
+
+    private val projectInfos = listOf(
+        MppProjectInfo("mirai-core-api"), MppProjectInfo("mirai-core-utils"),
+        JvmProjectInfo("mirai-console"), JvmProjectInfo("mirai-console-terminal")
+    ).associateBy { it.name }
+
+    private val ProjectInfo.compileTasks: Array<String>
+        get() = if (isMpp) {
+            arrayOf(":$name:jvmMainClasses", ":$name:androidMainClasses")
+        } else arrayOf(":$name:classes")
+
+    private fun getCompiledClassesPath(project: Project, info: ProjectInfo): Sequence<Sequence<File>> =
+        if (info.isMpp) {
+            sequenceOf("kotlin/jvm/main", "kotlin/android/main")
+        } else {
+            sequenceOf("kotlin/main")
+        }.map { sequenceOf(project.buildDir.resolve("classes").resolve(it)) }
+
+    private fun getLibraries(project: Project, info: ProjectInfo): Sequence<Sequence<File>> =
+        if (info.isMpp) {
+            sequenceOf("jvmCompileClasspath", "androidCompileClasspath")
+        } else {
+            sequenceOf("compileClasspath")
+        }.map { project.configurations.getByName(it).files.asSequence() }
+
+    fun Project.registerVerifyTask(taskName: String, action: VerifyAction) {
+
+        val projectInfo = projectInfos[this.name] ?: error("Project info of $name not found")
+
+        tasks.register(taskName) {
+            group = VERIFICATION_GROUP_NAME
+            mustRunAfter(*projectInfo.compileTasks)
+
+            doFirst {
+                getCompiledClassesPath(project, projectInfo).zip(getLibraries(project, projectInfo))
+                    .forEach { (compiledClasses, libraries) ->
+                        action(compiledClasses, libraries)
+                    }
+            }
+        }
+
+        tasks.named("check").configure { dependsOn(taskName) }
+        rootProject.tasks.getByName(RUN_ALL_VERITY_TASK_NAME).dependsOn(":$name:$taskName")
+    }
+
+    private fun Project.registerVerifyTasks() { // for feature extends
+        // https://github.com/mamoe/mirai/pull/1080#issuecomment-801197312
+        if (name != "mirai-console") {
+            registerVerifyTask("verify_NoNoSuchMethodError", NoSuchMethodAnalyzer::check)
+        }
+    }
+
+    fun Project/*RootProject*/.registerAllVerifyTasks() {
+        tasks.register(RUN_ALL_VERITY_TASK_NAME) {
+            group = VERIFICATION_GROUP_NAME
+        }
+        projectInfos.keys.forEach { projectName ->
+            findProject(projectName)?.let { subProject ->
+                subProject.afterEvaluate { subProject.registerVerifyTasks() }
+            }
+        }
+    }
+
+}

+ 90 - 0
buildSrc/src/main/kotlin/analyzes/NoSuchMethodAnalyzer.kt

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ *  https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package analyzes
+
+import analyzes.AsmUtil.hasField
+import analyzes.AsmUtil.hasMethod
+import org.objectweb.asm.tree.FieldInsnNode
+import org.objectweb.asm.tree.MethodInsnNode
+import org.objectweb.asm.tree.MethodNode
+import java.io.File
+import java.util.zip.ZipFile
+
+@Suppress("UNCHECKED_CAST")
+object NoSuchMethodAnalyzer {
+    private fun analyzeMethod(
+        analyzer: AndroidApiLevelCheck.Analyzer,
+        method: MethodNode,
+        asmClasses: AsmClassesM
+    ) {
+        analyzer.withContext("Analyze ${method.name}${method.desc}") {
+            method.instructions?.forEach { insn ->
+                when (insn) {
+                    is MethodInsnNode -> {
+                        if (insn.owner.startsWith("net/mamoe/mirai/")) {
+                            if (!asmClasses.hasMethod(insn.owner, insn.name, insn.desc, insn.opcode)) {
+                                report(
+                                    "No such method",
+                                    "${insn.owner}.${insn.name}${insn.desc}, opcode=${insn.opcode}"
+                                )
+                            }
+                        }
+                    }
+                    is FieldInsnNode -> {
+                        if (insn.owner.startsWith("net/mamoe/mirai/")) {
+                            if (!asmClasses.hasField(insn.owner, insn.name, insn.desc, insn.opcode)) {
+                                report(
+                                    "No such field",
+                                    "${insn.owner}.${insn.name}: ${insn.desc}, opcode=${insn.opcode}"
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    fun check(classes: Sequence<File>, libs: Sequence<File>) = AsmUtil.run {
+        val analyzer = AndroidApiLevelCheck.Analyzer(emptyMap())
+        val asmClasses: AsmClassesM = mutableMapOf()
+        libs.forEach { lib ->
+            if (lib.name.endsWith(".jar")) {
+                ZipFile(lib).use { zip ->
+                    zip.entries().iterator().forEach l@{ entry ->
+                        if (entry.isDirectory) return@l
+                        if (!entry.name.endsWith(".class")) return@l
+                        zip.getInputStream(entry).use { it.readClass() }.let {
+                            asmClasses[it.name] = it
+                        }
+                    }
+                }
+            } else if (lib.isDirectory) {
+                lib.walk().filter { it.isFile && it.extension == "class" }.forEach { f ->
+                    f.readClass().let { asmClasses[it.name] = it }
+                }
+            }
+        }
+        classes.map { it.walk() }.flatten().filter { it.isFile }
+            .filter { it.extension == "class" }
+            .map { it.readClass() to it }
+            .onEach { (c, _) ->
+                asmClasses[c.name] = c
+            }.toList().forEach { (classNode, file) ->
+                analyzer.file = file
+                classNode.methods?.forEach { method ->
+                    analyzeMethod(analyzer, method, asmClasses)
+                }
+            }
+        if (analyzer.reported) {
+            error("Verify failed")
+        }
+    }
+}

+ 1 - 1
mirai-core-api/build.gradle.kts

@@ -103,7 +103,7 @@ kotlin {
 
 tasks.register("checkAndroidApiLevel") {
     doFirst {
-        androidutil.AndroidApiLevelCheck.check(
+        analyzes.AndroidApiLevelCheck.check(
             buildDir.resolve("classes/kotlin/android/main"),
             project.property("mirai.android.target.api.level")!!.toString().toInt(),
             project

+ 1 - 1
mirai-core-utils/build.gradle.kts

@@ -91,7 +91,7 @@ kotlin {
 
 tasks.register("checkAndroidApiLevel") {
     doFirst {
-        androidutil.AndroidApiLevelCheck.check(
+        analyzes.AndroidApiLevelCheck.check(
             buildDir.resolve("classes/kotlin/android/main"),
             project.property("mirai.android.target.api.level")!!.toString().toInt(),
             project

+ 1 - 1
mirai-core/build.gradle.kts

@@ -116,7 +116,7 @@ kotlin {
 
 tasks.register("checkAndroidApiLevel") {
     doFirst {
-        androidutil.AndroidApiLevelCheck.check(
+        analyzes.AndroidApiLevelCheck.check(
             buildDir.resolve("classes/kotlin/android/main"),
             project.property("mirai.android.target.api.level")!!.toString().toInt(),
             project