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>
This commit is contained in:
Karlatemp 2021-03-20 13:34:04 +08:00 committed by GitHub
parent 77122dd445
commit f9353b6aef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 292 additions and 10 deletions

View File

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

View File

@ -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 }

View File

@ -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
}
}

View File

@ -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() }
}
}
}
}

View File

@ -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")
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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