diff --git a/module/incision/build.gradle.kts b/module/incision/build.gradle.kts index b12c474f8..d76713739 100644 --- a/module/incision/build.gradle.kts +++ b/module/incision/build.gradle.kts @@ -14,4 +14,15 @@ dependencies { compileOnly("org.ow2.asm:asm-util:9.8") // self-attach 默认路径 compileOnly("net.bytebuddy:byte-buddy-agent:1.14.18") + // 测试 + testImplementation(project(":common")) + testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") + // BodiesClassGenerator 在运行期直接使用 ASM,测试需要 ASM 运行时 + testImplementation("org.ow2.asm:asm:9.8") + testImplementation("org.ow2.asm:asm-commons:9.8") + testImplementation("org.ow2.asm:asm-util:9.8") +} + +tasks.test { + useJUnitPlatform() } diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt index 51a76971f..a7e39e2b7 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt @@ -131,7 +131,9 @@ object IncisionBootstrap { }.getOrNull() ?: "?" Forensics.debug("asm-tree probe: loader=${cl.javaClass.name} core=$asmCoreLoc tree=$asmTreeLoc") try { - val bytes = cl.getResourceAsStream("taboolib/module/incision/IncisionBootstrap.class")?.use { it.readBytes() } + // 资源路径从实际类对象推导,避免 const/字面量被 relocation 改写成点斜混合的非法路径 + val resourcePath = IncisionBootstrap::class.java.name.replace('.', '/') + ".class" + val bytes = cl.getResourceAsStream(resourcePath)?.use { it.readBytes() } if (bytes != null) { val node = org.objectweb.asm.tree.ClassNode() org.objectweb.asm.ClassReader(bytes).accept(node, 0) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt index 4c3069dec..68649c28f 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt @@ -21,6 +21,9 @@ object JvmtiBackend : Backend { private val transformers = ConcurrentHashMap ByteArray?>>() + /** 防重入保护:weave 过程中触发的类加载不应再次进入 transformer */ + private val reentrantGuard = ThreadLocal.withInitial { false } + @Volatile private var loaded = false @Volatile private var available = false @@ -59,19 +62,26 @@ object JvmtiBackend : Backend { /** Called from native ClassFileLoadHook for every (re)loaded class. */ @JvmStatic fun onClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? { + // 防重入:如果当前线程正在 weave 中(触发了新类加载),跳过 transformer 避免无限递归 + if (reentrantGuard.get()) return null val list = transformers[name] if (list == null) return null Forensics.debug("JvmtiBackend.onClassFileLoad: $name (${list.size} transformers, ${bytes.size} bytes)") - var cur = bytes - var changed = false - for (t in list) { - val out = try { t(cur) } catch (e: Throwable) { - Forensics.error("JvmtiBackend transformer error: $name", e); null + reentrantGuard.set(true) + try { + var cur = bytes + var changed = false + for (t in list) { + val out = try { t(cur) } catch (e: Throwable) { + Forensics.error("JvmtiBackend transformer error: $name", e); null + } + if (out != null) { cur = out; changed = true } } - if (out != null) { cur = out; changed = true } + Forensics.debug("JvmtiBackend.onClassFileLoad: $name → changed=$changed (${cur.size} bytes)") + return if (changed) cur else null + } finally { + reentrantGuard.set(false) } - Forensics.debug("JvmtiBackend.onClassFileLoad: $name → changed=$changed (${cur.size} bytes)") - return if (changed) cur else null } // ---------------------------------------------------------------- diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt index b8cdf9ae0..6a5c3cecb 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt @@ -114,6 +114,19 @@ object BodiesClassGenerator { private fun generateBodyMethod(original: MethodNode, ownerInternal: String, privateFields: Map): MethodNode? { if (original.access and ACC_STATIC != 0) return null + // 跳过构造函数 / 静态初始化块: + // 1. JVM 方法名规范禁止普通方法名包含 '<' '>',"$body" / "$body" 属非法方法名, + // 直接生成会触发 ClassFormatError: Illegal method name。 + // 2. 构造函数体通常包含对 super()/this() 的 INVOKESPECIAL,无法在 static body 上下文中安全复制。 + // 这类目标由 Incision 运行期回退到 IncisionBridge.dispatch 反射分发处理,无需 side-car body。 + if (original.name == "" || original.name == "") { + Forensics.debug( + "BodiesClassGenerator: 跳过 ${ownerInternal}.${original.name}${original.desc} " + + "(构造/静态初始化方法不生成 side-car body)" + ) + return null + } + val argTypes = Type.getArgumentTypes(original.desc) val returnType = Type.getReturnType(original.desc) @@ -186,8 +199,20 @@ object BodiesClassGenerator { return false } - /** JvmtiBackend 在 JNI 注册后的内部类名(可能被 Shadow 重定位) */ - private const val JVMTI_BACKEND = "taboolib/module/incision/loader/JvmtiBackend" + /** + * JvmtiBackend 的 JVM 内部类名(全斜杠形式)。 + * + * 注意:绝不能写成 `const val "taboolib/module/incision/loader/JvmtiBackend"`。 + * 因为 const 字符串会被内联,TabooLib 的 Shadow relocation 会把其中的 `taboolib` + * token 按「包名(点号)」规则替换为重定位前缀,得到点斜混合的非法名,例如 + * `group.taboolib/module/incision/loader/JvmtiBackend`,导致生成的 Bodies 类在 + * defineClass 时抛 ClassFormatError: Illegal class name。 + * + * 改为运行期从已重定位的实际类对象推导:`Class.name` 在重定位后是正确的全限定名, + * 仅需把 `.` 换成 `/` 即可得到合法内部名,且对任意重定位前缀都成立。 + */ + private val JVMTI_BACKEND: String = + taboolib.module.incision.loader.JvmtiBackend::class.java.name.replace('.', '/') /** * 克隆原方法指令流到 [out],做 slot 偏移、return 替换、private 字段访问替换。 diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt new file mode 100644 index 000000000..705289bf2 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt @@ -0,0 +1,209 @@ +package taboolib.module.incision.loader + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicInteger + +/** + * JvmtiBackend 防重入保护测试 + * + * 复现场景: + * Scalpel.weave(AdvancementDataPlayer) + * → RemapperBridge.mapFieldName + * → RemapTranslationLegacy.findParents + * → ClassHelper.getClass → Class.forName + * → ClassLoader.defineClass1 (触发 JVMTI ClassFileLoadHook) + * → JvmtiBackend.onClassFileLoad (再次进入 transformer) + * → Scalpel.weave → ... StackOverflowError + */ +@DisplayName("JvmtiBackend 防重入保护") +class JvmtiBackendReentrantTest { + + @Suppress("UNCHECKED_CAST") + private fun getTransformers(): ConcurrentHashMap ByteArray?>> { + val field = JvmtiBackend::class.java.getDeclaredField("transformers") + field.isAccessible = true + return field.get(JvmtiBackend) as ConcurrentHashMap ByteArray?>> + } + + private fun getReentrantGuard(): ThreadLocal { + val field = JvmtiBackend::class.java.getDeclaredField("reentrantGuard") + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + return field.get(JvmtiBackend) as ThreadLocal + } + + @BeforeEach + fun reset() { + getReentrantGuard().remove() + getTransformers().clear() + } + + // ===== 基础功能 ===== + + @Test + @DisplayName("正常情况下 transformer 被调用并返回修改后的字节") + fun normalTransformerInvocation() { + val callCount = AtomicInteger(0) + val transformers = getTransformers() + transformers.computeIfAbsent("com/example/TestClass") { mutableListOf() } + .add { bytes -> callCount.incrementAndGet(); bytes } + + val result = JvmtiBackend.onClassFileLoad(null, "com/example/TestClass", byteArrayOf(1, 2, 3)) + + assertEquals(1, callCount.get()) + assertNotNull(result) + } + + @Test + @DisplayName("无注册 transformer 时返回 null 不做任何处理") + fun noTransformerReturnsNull() { + val result = JvmtiBackend.onClassFileLoad(null, "com/example/Unknown", byteArrayOf(1)) + assertNull(result) + } + + // ===== 核心:递归地狱复现 ===== + + @Test + @DisplayName("互相触发类加载的两个 transformer 形成 A→B→A 递归,防重入保护打断递归链") + fun recursiveTransformerHellIsPrevented() { + val callCountA = AtomicInteger(0) + val callCountB = AtomicInteger(0) + val transformers = getTransformers() + + // transformer B:处理 InnerClass 时反向触发 OuterClass 加载 + transformers.computeIfAbsent("com/example/InnerClass") { mutableListOf() } + .add { bytes -> + callCountB.incrementAndGet() + JvmtiBackend.onClassFileLoad(null, "com/example/OuterClass", byteArrayOf(0xCA.toByte())) + bytes + } + + // transformer A:处理 OuterClass 时触发 InnerClass 加载 + transformers.computeIfAbsent("com/example/OuterClass") { mutableListOf() } + .add { bytes -> + callCountA.incrementAndGet() + JvmtiBackend.onClassFileLoad(null, "com/example/InnerClass", byteArrayOf(0xFE.toByte())) + bytes + } + + // 入口:加载 OuterClass + val result = JvmtiBackend.onClassFileLoad(null, "com/example/OuterClass", byteArrayOf(1, 2, 3)) + + // A 执行 1 次,B 被防重入拦截不执行 + assertEquals(1, callCountA.get()) + assertEquals(0, callCountB.get()) + assertNotNull(result) + } + + @Test + @DisplayName("单类自引用递归(transformer 内部再次触发同一类的 onClassFileLoad)被拦截") + fun selfRecursiveTransformerIsPrevented() { + val callCount = AtomicInteger(0) + val transformers = getTransformers() + + transformers.computeIfAbsent("com/example/SelfRef") { mutableListOf() } + .add { bytes -> + val depth = callCount.incrementAndGet() + if (depth > 100) { + fail("递归深度超过 100,防重入保护失效") + } + JvmtiBackend.onClassFileLoad(null, "com/example/SelfRef", bytes) + bytes + } + + val result = JvmtiBackend.onClassFileLoad(null, "com/example/SelfRef", byteArrayOf(1)) + + assertEquals(1, callCount.get()) + assertNotNull(result) + } + + @Test + @DisplayName("三层循环依赖 A→B→C→A 被防重入保护打断") + fun deepRecursiveChainIsBroken() { + val counts = ConcurrentHashMap() + val transformers = getTransformers() + + for (cls in listOf("com/example/A", "com/example/B", "com/example/C")) { + counts[cls] = AtomicInteger(0) + val nextCls = when (cls) { + "com/example/A" -> "com/example/B" + "com/example/B" -> "com/example/C" + else -> "com/example/A" + } + transformers.computeIfAbsent(cls) { mutableListOf() } + .add { bytes -> + counts[cls]!!.incrementAndGet() + JvmtiBackend.onClassFileLoad(null, nextCls, byteArrayOf(0)) + bytes + } + } + + JvmtiBackend.onClassFileLoad(null, "com/example/A", byteArrayOf(1)) + + assertEquals(1, counts["com/example/A"]!!.get()) + assertEquals(0, counts["com/example/B"]!!.get()) + assertEquals(0, counts["com/example/C"]!!.get()) + } + + // ===== 线程隔离 ===== + + @Test + @DisplayName("防重入标记是 ThreadLocal 的,不阻塞其他线程的正常 transformer 执行") + fun reentrantGuardIsPerThread() { + val threadBCount = AtomicInteger(0) + val latch = CountDownLatch(1) + val transformers = getTransformers() + + transformers.computeIfAbsent("com/example/SharedClass") { mutableListOf() } + .add { bytes -> threadBCount.incrementAndGet(); bytes } + + // 线程 A:手动设置重入标记模拟正在 weave + val threadA = Thread({ + getReentrantGuard().set(true) + val result = JvmtiBackend.onClassFileLoad(null, "com/example/SharedClass", byteArrayOf(1)) + assertNull(result) + latch.countDown() + }, "thread-A") + + // 线程 B:正常调用 + val threadB = Thread({ + latch.await() + val result = JvmtiBackend.onClassFileLoad(null, "com/example/SharedClass", byteArrayOf(2)) + assertNotNull(result) + }, "thread-B") + + threadA.start() + threadB.start() + threadA.join(5000) + threadB.join(5000) + + assertEquals(1, threadBCount.get()) + } + + // ===== 异常安全 ===== + + @Test + @DisplayName("transformer 抛异常后重入标记正确清除,后续调用不受影响") + fun reentrantGuardClearedAfterException() { + val transformers = getTransformers() + transformers.computeIfAbsent("com/example/ErrorClass") { mutableListOf() } + .add { throw RuntimeException("模拟异常") } + + // 第一次:异常 + JvmtiBackend.onClassFileLoad(null, "com/example/ErrorClass", byteArrayOf(1)) + + // 第二次:标记应已清除 + val secondCount = AtomicInteger(0) + transformers["com/example/ErrorClass"]!!.clear() + transformers["com/example/ErrorClass"]!!.add { bytes -> secondCount.incrementAndGet(); bytes } + + val result = JvmtiBackend.onClassFileLoad(null, "com/example/ErrorClass", byteArrayOf(2)) + assertEquals(1, secondCount.get()) + assertNotNull(result) + } +} diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt new file mode 100644 index 000000000..e0787c067 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt @@ -0,0 +1,248 @@ +package taboolib.module.incision.weaver + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes.ACC_PRIVATE +import org.objectweb.asm.Opcodes.ACC_PUBLIC +import org.objectweb.asm.Opcodes.ACC_STATIC +import org.objectweb.asm.Opcodes.ALOAD +import org.objectweb.asm.Opcodes.GETFIELD +import org.objectweb.asm.Opcodes.ICONST_0 +import org.objectweb.asm.Opcodes.INVOKESPECIAL +import org.objectweb.asm.Opcodes.IRETURN +import org.objectweb.asm.Opcodes.PUTFIELD +import org.objectweb.asm.Opcodes.RETURN +import org.objectweb.asm.Opcodes.V1_8 +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode + +/** + * BodiesClassGenerator 回归测试。 + * + * 覆盖两个曾导致 `XXX$$IncisionBodies` 在 defineClass 阶段抛 ClassFormatError 的缺陷: + * + * Bug 1(Illegal class name): + * JvmtiBackend 的内部名曾以 `const val "taboolib/module/incision/loader/JvmtiBackend"` + * 硬编码。const 会被内联,TabooLib Shadow relocation 把其中的 `taboolib` 段按「包名(点号)」 + * 规则替换为重定位前缀,当宿主插件 group 较长(多段点号)时得到点斜混合的非法内部名 + * `group.taboolib/module/incision/loader/JvmtiBackend`,被用作 INVOKESTATIC owner 后 + * defineClass 抛 `Illegal class name`。修复:改为运行期 `JvmtiBackend::class.java.name.replace('.', '/')`。 + * + * Bug 2(Illegal method name): + * 当被 @Splice 的目标方法是构造函数 `` / 静态初始化 `` 时, + * body 方法名曾直接拼成 `$body`,含非法字符 `<` `>`,defineClass 抛 `Illegal method name`。 + * 修复:跳过 `` / `` 的 body 生成(其方法体含 super()/this() INVOKESPECIAL, + * 本就无法在 static 上下文安全复制,运行期由 IncisionBridge.dispatch 兜底)。 + */ +@DisplayName("BodiesClassGenerator 非法名回归") +class BodiesClassGeneratorTest { + + private val ownerInternal = "taboolib/module/incision/weaver/sample/SampleTarget" + + /** + * 构造一个用于测试的目标类字节码: + * - 私有字段 `private int value` + * - 构造函数 `(int)`:super() + this.value = arg(含 INVOKESPECIAL super.) + * - 实例方法 `getValue()I`:return this.value(含 GETFIELD private 字段) + * - 静态方法 `staticHelper()I`:return 0 + */ + private fun buildSampleTargetBytes(): ByteArray { + val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + cw.visit(V1_8, ACC_PUBLIC, ownerInternal, null, "java/lang/Object", null) + + cw.visitField(ACC_PRIVATE, "value", "I", null, null).visitEnd() + + // 构造函数 (I)V + cw.visitMethod(ACC_PUBLIC, "", "(I)V", null, null).apply { + visitCode() + visitVarInsn(ALOAD, 0) + visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false) + visitVarInsn(ALOAD, 0) + visitVarInsn(org.objectweb.asm.Opcodes.ILOAD, 1) + visitFieldInsn(PUTFIELD, ownerInternal, "value", "I") + visitInsn(RETURN) + visitMaxs(0, 0) + visitEnd() + } + + // 实例方法 getValue()I + cw.visitMethod(ACC_PUBLIC, "getValue", "()I", null, null).apply { + visitCode() + visitVarInsn(ALOAD, 0) + visitFieldInsn(GETFIELD, ownerInternal, "value", "I") + visitInsn(IRETURN) + visitMaxs(0, 0) + visitEnd() + } + + // 静态方法 staticHelper()I + cw.visitMethod(ACC_STATIC or ACC_PUBLIC, "staticHelper", "()I", null, null).apply { + visitCode() + visitInsn(ICONST_0) + visitInsn(IRETURN) + visitMaxs(0, 0) + visitEnd() + } + + cw.visitEnd() + return cw.toByteArray() + } + + /** 简易类加载器:仅用于把生成的字节定义成 Class,触发 JVM 名称校验 */ + private class ByteClassLoader : ClassLoader(BodiesClassGeneratorTest::class.java.classLoader) { + fun define(name: String, bytes: ByteArray): Class<*> = defineClass(name, bytes, 0, bytes.size) + } + + private fun readNode(bytes: ByteArray): ClassNode { + val node = ClassNode() + ClassReader(bytes).accept(node, 0) + return node + } + + // ===== Bug 2:构造函数 / 静态初始化 不得生成非法方法名 ===== + + @Test + @DisplayName("目标含 时跳过 body 生成,绝不产生非法方法名 \$body") + fun constructorTargetDoesNotProduceIllegalMethodName() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("" to "(I)V") + ) + // 仅有 一个目标且被跳过 → 没有任何可生成的 body → 返回 null + assertNull(bytes, "仅构造函数作为目标时应无 body 生成,返回 null") + } + + @Test + @DisplayName(" 与普通方法混合时,仅普通方法生成 body,且无 \$body") + fun mixedTargetsSkipConstructorOnly() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("" to "(I)V", "getValue" to "()I") + ) + assertNotNull(bytes, "存在普通方法目标时应生成 bodies 类") + + val node = readNode(bytes!!) + val methodNames = node.methods.map { it.name } + // 不允许出现任何含 '<' '>' 的非法方法名 + assertFalse( + methodNames.any { it.contains('<') || it.contains('>') }, + "生成的 bodies 方法名不得包含 '<' '>',实际: $methodNames" + ) + assertTrue(methodNames.contains("getValue\$body"), "普通方法应生成 getValue\$body,实际: $methodNames") + assertFalse(methodNames.contains("\$body"), "绝不允许生成 \$body") + } + + @Test + @DisplayName("生成的 bodies 类可被 defineClass 加载,不抛 ClassFormatError") + fun generatedBodiesClassIsLoadable() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("" to "(I)V", "getValue" to "()I") + ) + assertNotNull(bytes) + val node = readNode(bytes!!) + // defineClass 会触发 JVM 对类名/方法名/常量引用的合法性校验 + assertDoesNotThrow { + ByteClassLoader().define(node.name.replace('/', '.'), bytes) + } + } + + // ===== Bug 1:JvmtiBackend 引用必须为合法全斜杠内部名 ===== + + @Test + @DisplayName("body 中对 JvmtiBackend 的 INVOKESTATIC owner 为合法全斜杠内部名(无点斜混合)") + fun jvmtiBackendOwnerIsValidInternalName() { + // getValue 含 GETFIELD private 字段 → 会改写为 JvmtiBackend.nFieldGet 调用 + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("getValue" to "()I") + ) + assertNotNull(bytes) + val node = readNode(bytes!!) + + val jvmtiInvokes = node.methods + .flatMap { it.instructions.toArray().toList() } + .filterIsInstance() + .filter { it.owner.endsWith("/JvmtiBackend") || it.owner.endsWith(".JvmtiBackend") } + + assertTrue(jvmtiInvokes.isNotEmpty(), "getValue 改写后应至少有一处对 JvmtiBackend 的调用") + + jvmtiInvokes.forEach { insn -> + val owner = insn.owner + // 合法内部名:全斜杠、不含点号、不含点斜混合 + assertFalse(owner.contains('.'), "JvmtiBackend owner 不应含点号(点斜混合非法名): $owner") + assertTrue( + owner.endsWith("incision/loader/JvmtiBackend"), + "JvmtiBackend owner 应为全斜杠内部名,实际: $owner" + ) + } + } + + @Test + @DisplayName("含 private 字段访问的 body 改写后整体可加载(间接校验 JvmtiBackend 引用合法)") + fun bodyWithPrivateFieldAccessIsLoadable() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("getValue" to "()I") + ) + assertNotNull(bytes) + val node = readNode(bytes!!) + assertDoesNotThrow { + ByteClassLoader().define(node.name.replace('/', '.'), bytes) + } + } + + // ===== 行为基线:静态方法目标被忽略、空目标返回 null ===== + + @Test + @DisplayName("静态方法作为目标被忽略(static 方法无 self,不生成 body)") + fun staticMethodTargetIsIgnored() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("staticHelper" to "()I") + ) + assertNull(bytes, "静态方法不应生成 body") + } + + @Test + @DisplayName("空目标集合返回 null") + fun emptyTargetsReturnsNull() { + val bytes = generate(buildSampleTargetBytes(), targetMethods = emptySet()) + assertNull(bytes) + } + + @Test + @DisplayName("普通实例方法正常生成 body 且类名为 owner\$\$IncisionBodies") + fun normalMethodGeneratesBody() { + val bytes = generate( + buildSampleTargetBytes(), + targetMethods = setOf("getValue" to "()I") + ) + assertNotNull(bytes) + val node = readNode(bytes!!) + assertTrue( + node.name.endsWith("\$\$IncisionBodies"), + "bodies 类名应以 \$\$IncisionBodies 结尾,实际: ${node.name}" + ) + val body = node.methods.first { it.name == "getValue\$body" } + // 统一 body 描述符 (Object, Object[]) -> Object + assertTrue( + body.desc == "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", + "body 描述符应为统一签名,实际: ${body.desc}" + ) + assertTrue(body.access and ACC_STATIC != 0, "body 方法应为 static") + } + + // ===== 工具 ===== + + private fun generate(originalBytes: ByteArray, targetMethods: Set>): ByteArray? { + return BodiesClassGenerator.generate(originalBytes, ownerInternal, targetMethods) + } +}