From 88720d89a80c2d993651df49e2a9d606327f6310 Mon Sep 17 00:00:00 2001 From: Micalhl Date: Thu, 9 Apr 2026 22:16:37 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Reflex=20=E8=87=B3=201?= =?UTF-8?q?.2.4=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=8E=20JDK=208=20=E7=9A=84?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 8 +++---- common-reflex/build.gradle.kts | 8 +++---- .../taboolib/common/io/ProjectScanner.kt | 24 +++++-------------- .../java/taboolib/common/ClassAppender.java | 8 +++---- .../java/taboolib/common/PrimitiveLoader.java | 4 ++-- 5 files changed, 20 insertions(+), 32 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f28134c5b..30d2ec6e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,16 +31,16 @@ subprojects { compileOnly("com.google.guava:guava:21.0") compileOnly("com.google.code.gson:gson:2.8.7") compileOnly("org.apache.commons:commons-lang3:3.5") - compileOnly("org.tabooproject.reflex:reflex:1.2.3") - compileOnly("org.tabooproject.reflex:analyser:1.2.3") + compileOnly("org.tabooproject.reflex:reflex:1.2.4") + compileOnly("org.tabooproject.reflex:analyser:1.2.4") // 测试依赖 testImplementation(kotlin("stdlib")) testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") testImplementation("com.google.guava:guava:21.0") testImplementation("com.google.code.gson:gson:2.8.7") testImplementation("org.apache.commons:commons-lang3:3.5") - testImplementation("org.tabooproject.reflex:reflex:1.2.3") - testImplementation("org.tabooproject.reflex:analyser:1.2.3") + testImplementation("org.tabooproject.reflex:reflex:1.2.4") + testImplementation("org.tabooproject.reflex:analyser:1.2.4") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") } diff --git a/common-reflex/build.gradle.kts b/common-reflex/build.gradle.kts index 06f30215a..09e0ef8fb 100644 --- a/common-reflex/build.gradle.kts +++ b/common-reflex/build.gradle.kts @@ -1,15 +1,15 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar dependencies { - implementation("org.tabooproject.reflex:reflex:1.2.3") - implementation("org.tabooproject.reflex:analyser:1.2.3") + implementation("org.tabooproject.reflex:reflex:1.2.4") + implementation("org.tabooproject.reflex:analyser:1.2.4") } tasks { withType { dependencies { - include(dependency("org.tabooproject.reflex:reflex:1.2.3")) - include(dependency("org.tabooproject.reflex:analyser:1.2.3")) + include(dependency("org.tabooproject.reflex:reflex:1.2.4")) + include(dependency("org.tabooproject.reflex:analyser:1.2.4")) } relocate("org.taboooproject", "taboolib.library") } diff --git a/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt b/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt index ee994c470..1ed6d652a 100644 --- a/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt +++ b/common-util/src/main/kotlin/taboolib/common/io/ProjectScanner.kt @@ -139,14 +139,6 @@ var extraLoadedClasses = ConcurrentHashMap() */ var extraLoadedResources = ConcurrentHashMap() -/** - * Reflex 的二进制类缓存会在 JDK 8 读取时触发 ByteBuffer API 兼容问题, - * 因此在 JDK 8 下退回为即时扫描,避免第二次启动读取缓存时报错。 - */ -private val isClassBinaryCacheSupported by lazy(LazyThreadSafetyMode.NONE) { - System.getProperty("java.specification.version") != "1.8" -} - /** * 获取 URL 下的所有类 */ @@ -162,14 +154,12 @@ fun URL.getClasses(classLoader: ClassLoader = ClassAppender.getClassLoader()): M // 是文件 if (srcFile.isFile) { val srcVersion = srcFile.digest() - if (isClassBinaryCacheSupported) { - // 从二进制缓存中读取 - val classMap = BinaryCache.read(srcFile.nameWithoutExtension, srcVersion) { - ReflexClassMap.deserializeFromBytes(it) { name -> Class.forName(name, false, classLoader) } - // 注意:不再立即添加到 reflexClassCacheMap,延迟到访问时添加 - } - if (classMap != null) return classMap + // 从二进制缓存中读取 + val classMap = BinaryCache.read(srcFile.nameWithoutExtension, srcVersion) { + ReflexClassMap.deserializeFromBytes(it) { name -> Class.forName(name, false, classLoader) } + // 注意:不再立即添加到 reflexClassCacheMap,延迟到访问时添加 } + if (classMap != null) return classMap // 从文件中解析 JarFile(srcFile).use { jar -> jar.stream() @@ -182,9 +172,7 @@ fun URL.getClasses(classLoader: ClassLoader = ClassAppender.getClassLoader()): M } } // 保存 - if (isClassBinaryCacheSupported) { - BinaryCache.save(srcFile.nameWithoutExtension, srcVersion) { ReflexClassMap.serializeToBytes(classes) } - } + BinaryCache.save(srcFile.nameWithoutExtension, srcVersion) { ReflexClassMap.serializeToBytes(classes) } } // 是目录 else { diff --git a/common/src/main/java/taboolib/common/ClassAppender.java b/common/src/main/java/taboolib/common/ClassAppender.java index fb6774678..4945af6d5 100644 --- a/common/src/main/java/taboolib/common/ClassAppender.java +++ b/common/src/main/java/taboolib/common/ClassAppender.java @@ -108,15 +108,15 @@ private static void addURL(ClassLoader loader, Field ucpField, File file, boolea throw new IllegalStateException("lookup not found"); } Object ucp = unsafe.getObject(loader, unsafe.objectFieldOffset(ucpField)); - MethodHandle methodHandle = lookup.findVirtual(ucp.getClass(), "addURL", MethodType.methodType(void.class, URL.class)); try { + MethodHandle methodHandle = lookup.findVirtual(ucp.getClass(), "addURL", MethodType.methodType(void.class, URL.class)); methodHandle.invoke(ucp, file.toURI().toURL()); + for (Callback i : callbacks) { + i.add(loader, file, isExternal); + } } catch (NoSuchMethodError e) { throw new IllegalStateException("Unsupported (classloader: " + loader.getClass().getName() + ", ucp: " + ucp.getClass().getName() + ")", e); } - for (Callback i : callbacks) { - i.add(loader, file, isExternal); - } } private static Field ucp(ClassLoader loader) { diff --git a/common/src/main/java/taboolib/common/PrimitiveLoader.java b/common/src/main/java/taboolib/common/PrimitiveLoader.java index 470584440..165e190a6 100644 --- a/common/src/main/java/taboolib/common/PrimitiveLoader.java +++ b/common/src/main/java/taboolib/common/PrimitiveLoader.java @@ -97,8 +97,8 @@ public static void init() throws Throwable { load(REPO_CENTRAL, i[0], i[1], i[2], IS_ISOLATED_MODE, true, rule()); } // 加载反射模块 - load(REPO_REFLEX, TABOOPROJECT_GROUP + ".reflex", "reflex", "1.2.3", IS_ISOLATED_MODE, true, rule()); - load(REPO_REFLEX, TABOOPROJECT_GROUP + ".reflex", "analyser", "1.2.3", IS_ISOLATED_MODE, true, rule()); + load(REPO_REFLEX, TABOOPROJECT_GROUP + ".reflex", "reflex", "1.2.4", IS_ISOLATED_MODE, true, rule()); + load(REPO_REFLEX, TABOOPROJECT_GROUP + ".reflex", "analyser", "1.2.4", IS_ISOLATED_MODE, true, rule()); }); PrimitiveIO.debug("基础依赖加载完成,用时 {0} 毫秒。", time); // 加载完整模块 From 716e043af421ce7f3093e7bead4ff694b64465b4 Mon Sep 17 00:00:00 2001 From: Micalhl Date: Tue, 14 Apr 2026 14:02:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=94=AF=E6=8C=81=2026.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/taboolib/module/nms/MinecraftVersion.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/MinecraftVersion.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/MinecraftVersion.kt index e1c8040f7..a138ff617 100644 --- a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/MinecraftVersion.kt +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/MinecraftVersion.kt @@ -102,7 +102,7 @@ object MinecraftVersion { arrayOf("1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4"), // 11 arrayOf("1.20", "1.20.1", "1.20.2", "!1.20.3", "1.20.4", "!1.20.5", "1.20.6"), // 12 (跳过 1.20.3、1.20.5) NOTICE 从 1.20.5 开始, paper 进行了破坏性修改 arrayOf("!1.21", "1.21.1", "!1.21.2", "1.21.3", "1.21.4", "1.21.5", "!1.21.6", "!1.21.7", "1.21.8", "!1.21.9", "1.21.10", "1.21.11"), // 13 (跳过 1.21、1.21.2、1.21.6、1.21.7 和 1.21.9) - arrayOf("26.1", "26.1.1") // 14 NOTICE 从 26.1 开始, Minecraft 不再被混淆 + arrayOf("!26.1", "!26.1.1", "26.1.2") // 14 (跳过 26.1、26.1.1) NOTICE 从 26.1 开始, Minecraft 不再被混淆 // @formatter:on ) From 53fee2368d34a2ab8af345482daee3dec6249345 Mon Sep 17 00:00:00 2001 From: FxRayHughes Date: Mon, 20 Apr 2026 17:16:40 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(incision):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=97=E8=8A=82=E7=A0=81=E7=BB=87=E5=85=A5=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=B8=8E=20Accessor=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ray_Hughes --- .gitignore | 4 +- .../java/taboolib/common/PrimitiveLoader.java | 3 + .../module/nms/AsmClassTranslation.kt | 2 + .../module/nms/remap/RemapTranslation.kt | 33 +- module/incision/README.md | 547 ++++++++ module/incision/TECHNICAL.md | 581 +++++++++ module/incision/build.gradle.kts | 17 + module/incision/src/main/c/build-all.bat | 51 + module/incision/src/main/c/incision_jvmti.c | 1162 +++++++++++++++++ .../src/main/c/include/darwin/jni_md.h | 14 + .../src/main/c/include/linux/jni_md.h | 18 + .../izzel/incision/bridge/IncisionBridge.java | 202 +++ .../incision/bridge/IncisionGateHost.java | 107 ++ .../loader/attach/IncisionSelfAgent.java | 40 + .../module/incision/pred/PredOps.java | 260 ++++ .../module/incision/IncisionBootstrap.kt | 210 +++ .../module/incision/annotation/Bypass.kt | 36 + .../module/incision/annotation/Excise.kt | 32 + .../module/incision/annotation/Graft.kt | 36 + .../module/incision/annotation/InsnPattern.kt | 26 + .../incision/annotation/KotlinTarget.kt | 28 + .../module/incision/annotation/Lead.kt | 35 + .../taboolib/module/incision/annotation/Op.kt | 209 +++ .../module/incision/annotation/Operation.kt | 35 + .../module/incision/annotation/Scope.kt | 30 + .../module/incision/annotation/Site.kt | 37 + .../module/incision/annotation/Splice.kt | 34 + .../module/incision/annotation/Step.kt | 36 + .../module/incision/annotation/Surgeon.kt | 28 + .../module/incision/annotation/SurgeryDesk.kt | 27 + .../module/incision/annotation/Trail.kt | 36 + .../module/incision/annotation/Trim.kt | 47 + .../module/incision/annotation/Version.kt | 38 + .../taboolib/module/incision/api/Accessors.kt | 159 +++ .../taboolib/module/incision/api/Anatomy.kt | 15 + .../taboolib/module/incision/api/Anchor.kt | 30 + .../module/incision/api/DescriptorCodec.kt | 78 ++ .../module/incision/api/IncisionAccessor.kt | 311 +++++ .../module/incision/api/MethodCoordinate.kt | 21 + .../taboolib/module/incision/api/Resume.kt | 26 + .../taboolib/module/incision/api/Shift.kt | 12 + .../taboolib/module/incision/api/Suture.kt | 66 + .../taboolib/module/incision/api/Theatre.kt | 139 ++ .../module/incision/api/VersionMatcher.kt | 158 +++ .../module/incision/cache/IncisionCache.kt | 93 ++ .../command/IncisionCommandHandler.kt | 71 + .../module/incision/diagnostic/Checkup.kt | 40 + .../incision/diagnostic/ConflictAnalyzer.kt | 73 ++ .../module/incision/diagnostic/Forensics.kt | 54 + .../module/incision/diagnostic/Trauma.kt | 222 ++++ .../module/incision/dsl/ArmTrigger.kt | 43 + .../taboolib/module/incision/dsl/Scalpel.kt | 279 ++++ .../module/incision/dsl/ScalpelBuilder.kt | 114 ++ .../module/incision/dsl/ScopedHandle.kt | 60 + .../module/incision/dsl/SutureImpl.kt | 52 + .../incision/gate/DefaultIncisionGate.kt | 141 ++ .../module/incision/gate/GateBootstrapper.kt | 210 +++ .../module/incision/gate/IncisionGateApi.kt | 61 + .../incision/gate/IncisionGateLocator.kt | 89 ++ .../incision/lifecycle/AutoHealHandler.kt | 50 + .../module/incision/loader/Backend.kt | 26 + .../incision/loader/ClassLoaderHookBackend.kt | 47 + .../incision/loader/InstrumentationBackend.kt | 122 ++ .../module/incision/loader/JvmtiBackend.kt | 298 +++++ .../module/incision/loader/PipelineBackend.kt | 83 ++ .../module/incision/loader/SurgeonScanner.kt | 560 ++++++++ .../loader/attach/ByteBuddyAttacher.kt | 22 + .../loader/attach/ManualSelfAttach.kt | 134 ++ .../module/incision/pred/AdviceCtx.kt | 16 + .../module/incision/pred/EvalContext.kt | 36 + .../module/incision/pred/EvalContextImpl.kt | 52 + .../taboolib/module/incision/pred/PredAst.kt | 59 + .../module/incision/pred/PredCompiler.kt | 496 +++++++ .../module/incision/pred/PredLexer.kt | 148 +++ .../module/incision/pred/PredParser.kt | 281 ++++ .../module/incision/pred/Predicate.kt | 17 + .../module/incision/proxy/AdviceProxy.kt | 50 + .../module/incision/reflex/IncisionReflex.kt | 135 ++ .../module/incision/remap/NameResolver.kt | 31 + .../module/incision/remap/NoopResolver.kt | 17 + .../module/incision/remap/RemapRouter.kt | 33 + .../incision/remap/TabooLibNmsResolver.kt | 148 +++ .../module/incision/runtime/AdviceChain.kt | 68 + .../runtime/DispatchSignatureCodec.kt | 63 + .../incision/runtime/SurgeryRegistry.kt | 53 + .../incision/runtime/TheatreDispatcher.kt | 383 ++++++ .../module/incision/scope/ScopeParser.kt | 204 +++ .../incision/weaver/BodiesClassGenerator.kt | 397 ++++++ .../incision/weaver/BridgeClassLoader.kt | 58 + .../module/incision/weaver/FrameVerifier.kt | 72 + .../module/incision/weaver/Scalpel.kt | 520 ++++++++ .../module/incision/weaver/SiteWeaver.kt | 984 ++++++++++++++ .../module/incision/weaver/site/SiteSpec.kt | 64 + .../weaver/site/emitter/DispatcherEmitter.kt | 448 +++++++ .../weaver/site/matcher/FieldMatcher.kt | 45 + .../weaver/site/matcher/InsnStepMatcher.kt | 42 + .../weaver/site/matcher/InvokeMatcher.kt | 42 + .../weaver/site/matcher/NewMatcher.kt | 38 + .../weaver/site/matcher/OpcodeSeqMatcher.kt | 106 ++ .../weaver/site/matcher/SiteMatcher.kt | 36 + .../weaver/site/matcher/TerminalMatcher.kt | 55 + .../incision/weaver/site/pattern/InsnStep.kt | 23 + .../weaver/site/pattern/SiteOffset.kt | 24 + .../weaver/site/pattern/SitePattern.kt | 58 + .../site/pattern/parser/FieldTargetParser.kt | 44 + .../site/pattern/parser/InvokeTargetParser.kt | 25 + .../site/pattern/parser/NewTargetParser.kt | 20 + .../site/pattern/parser/NoTargetParser.kt | 23 + .../site/pattern/parser/ParserRegistry.kt | 57 + .../site/pattern/parser/SiteTargetParser.kt | 23 + .../weaver/site/planner/EmissionPlan.kt | 23 + .../weaver/site/planner/PlannerDispatcher.kt | 57 + .../weaver/site/recorder/InsnAction.kt | 204 +++ .../site/recorder/RecordingMethodVisitor.kt | 172 +++ .../incision/weaver/site/recorder/Replayer.kt | 68 + .../native/linux/arm64/libincision-jvmti.so | Bin 0 -> 101216 bytes .../native/linux/x64/libincision-jvmti.so | Bin 0 -> 102840 bytes .../macos/arm64/libincision-jvmti.dylib | Bin 0 -> 69272 bytes .../native/macos/x64/libincision-jvmti.dylib | Bin 0 -> 27796 bytes .../native/windows/arm64/incision-jvmti.dll | Bin 0 -> 182784 bytes .../native/windows/arm64/incision-jvmti.pdb | Bin 0 -> 1228800 bytes .../native/windows/arm64/incision_jvmti.lib | Bin 0 -> 1486 bytes .../native/windows/x64/incision-jvmti.dll | Bin 0 -> 199680 bytes .../native/windows/x64/incision-jvmti.pdb | Bin 0 -> 1081344 bytes .../native/windows/x64/incision_jvmti.lib | Bin 0 -> 1486 bytes .../main/skills/taboolib-incision/SKILL.md | 674 ++++++++++ .../taboolib-incision/agents/openai.yaml | 7 + settings.gradle.kts | 3 + 128 files changed, 14660 insertions(+), 2 deletions(-) create mode 100644 module/incision/README.md create mode 100644 module/incision/TECHNICAL.md create mode 100644 module/incision/build.gradle.kts create mode 100644 module/incision/src/main/c/build-all.bat create mode 100644 module/incision/src/main/c/incision_jvmti.c create mode 100644 module/incision/src/main/c/include/darwin/jni_md.h create mode 100644 module/incision/src/main/c/include/linux/jni_md.h create mode 100644 module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java create mode 100644 module/incision/src/main/java/io/izzel/incision/bridge/IncisionGateHost.java create mode 100644 module/incision/src/main/java/taboolib/module/incision/loader/attach/IncisionSelfAgent.java create mode 100644 module/incision/src/main/java/taboolib/module/incision/pred/PredOps.java create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Bypass.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Excise.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Graft.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/InsnPattern.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/KotlinTarget.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Lead.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Op.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Operation.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Scope.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Site.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Splice.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Step.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Surgeon.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/SurgeryDesk.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trail.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trim.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/annotation/Version.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Accessors.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Anatomy.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Anchor.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/DescriptorCodec.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/IncisionAccessor.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/MethodCoordinate.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Resume.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Shift.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Suture.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/Theatre.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/api/VersionMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/cache/IncisionCache.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/command/IncisionCommandHandler.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Checkup.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/ConflictAnalyzer.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Trauma.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/dsl/ArmTrigger.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScalpelBuilder.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScopedHandle.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/dsl/SutureImpl.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/gate/DefaultIncisionGate.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateApi.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateLocator.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/lifecycle/AutoHealHandler.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/Backend.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/ClassLoaderHookBackend.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/InstrumentationBackend.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/PipelineBackend.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/SurgeonScanner.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ByteBuddyAttacher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ManualSelfAttach.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContext.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContextImpl.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/PredAst.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/PredLexer.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/PredParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/proxy/AdviceProxy.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/reflex/IncisionReflex.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/remap/NameResolver.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/remap/NoopResolver.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/remap/RemapRouter.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/remap/TabooLibNmsResolver.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/runtime/DispatchSignatureCodec.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/runtime/SurgeryRegistry.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/scope/ScopeParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/BridgeClassLoader.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/FrameVerifier.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/SiteWeaver.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/SiteSpec.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/emitter/DispatcherEmitter.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/FieldMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InsnStepMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InvokeMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/NewMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/OpcodeSeqMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/SiteMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/TerminalMatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/InsnStep.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SiteOffset.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SitePattern.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/FieldTargetParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/InvokeTargetParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NewTargetParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NoTargetParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/ParserRegistry.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/SiteTargetParser.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/EmissionPlan.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/PlannerDispatcher.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/InsnAction.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/RecordingMethodVisitor.kt create mode 100644 module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/Replayer.kt create mode 100644 module/incision/src/main/resources/native/linux/arm64/libincision-jvmti.so create mode 100644 module/incision/src/main/resources/native/linux/x64/libincision-jvmti.so create mode 100644 module/incision/src/main/resources/native/macos/arm64/libincision-jvmti.dylib create mode 100644 module/incision/src/main/resources/native/macos/x64/libincision-jvmti.dylib create mode 100644 module/incision/src/main/resources/native/windows/arm64/incision-jvmti.dll create mode 100644 module/incision/src/main/resources/native/windows/arm64/incision-jvmti.pdb create mode 100644 module/incision/src/main/resources/native/windows/arm64/incision_jvmti.lib create mode 100644 module/incision/src/main/resources/native/windows/x64/incision-jvmti.dll create mode 100644 module/incision/src/main/resources/native/windows/x64/incision-jvmti.pdb create mode 100644 module/incision/src/main/resources/native/windows/x64/incision_jvmti.lib create mode 100644 module/incision/src/main/skills/taboolib-incision/SKILL.md create mode 100644 module/incision/src/main/skills/taboolib-incision/agents/openai.yaml diff --git a/.gitignore b/.gitignore index b10273c38..caa1259dd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ bin .settings .classpath .project -doc \ No newline at end of file +doc +.claude/ +nul diff --git a/common/src/main/java/taboolib/common/PrimitiveLoader.java b/common/src/main/java/taboolib/common/PrimitiveLoader.java index 165e190a6..0c85b27ee 100644 --- a/common/src/main/java/taboolib/common/PrimitiveLoader.java +++ b/common/src/main/java/taboolib/common/PrimitiveLoader.java @@ -56,6 +56,9 @@ static List deps() { deps.add(new String[]{"!org.ow2.asm".substring(1), "asm", "9.8"}); deps.add(new String[]{"!org.ow2.asm".substring(1), "asm-util", "9.8"}); deps.add(new String[]{"!org.ow2.asm".substring(1), "asm-commons", "9.8"}); + // 都是必要的 incision 强制依赖 + deps.add(new String[]{"!org.ow2.asm".substring(1), "asm-tree", "9.8"}); + deps.add(new String[]{"!org.ow2.asm".substring(1), "asm-analysis", "9.8"}); return deps; } diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt index 174b22350..6318dd554 100644 --- a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/AsmClassTranslation.kt @@ -85,6 +85,8 @@ class AsmClassTranslation(val source: String) { newBytes = remapper.applyRequireTransform(newBytes) // 应用 dynamic 转换(检测并替换 dynamic() 调用为直接 JVM 指令) newBytes = remapper.applyDynamicTransform(newBytes) + // 应用额外 transformer(incision 等模块在此织入) + newBytes = remapper.applyExtraTransforms(source, newBytes) // 缓存 BinaryCache.save("remap/$source", combinedVersion) { newBytes } // 保存字节码用于调试 diff --git a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/remap/RemapTranslation.kt b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/remap/RemapTranslation.kt index aef7a3aa0..3b922c259 100644 --- a/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/remap/RemapTranslation.kt +++ b/module/bukkit-nms/src/main/kotlin/taboolib/module/nms/remap/RemapTranslation.kt @@ -6,6 +6,7 @@ import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes import taboolib.common.reflect.ClassHelper import taboolib.module.nms.MinecraftVersion +import java.util.concurrent.CopyOnWriteArrayList /** * 插件内部的类 @@ -29,6 +30,36 @@ open class RemapTranslation : Remapper() { val obc2 = "org/bukkit/craftbukkit/${MinecraftVersion.minecraftVersion}/" val obc3 = "org/bukkit/craftbukkit/" + companion object { + + /** + * 额外的字节码变换器 —— 在 remap + require + dynamic 三步之后执行。 + * + * 供 [taboolib.module.incision] 等模块在 NMSProxy 管线末端注入自定义织入逻辑。 + * 每个 transformer 的签名:`(className, bytes) -> ByteArray?`; + * 返回 null 表示不修改,返回非 null 则替换当前字节码继续传递下一个 transformer。 + * + * 线程安全:使用 [java.util.concurrent.CopyOnWriteArrayList]。 + */ + @JvmStatic + val extraTransformers: MutableList<(String, ByteArray) -> ByteArray?> = CopyOnWriteArrayList() + } + + /** 运行 [extraTransformers] 管线;供 [taboolib.module.nms.AsmClassTranslation] 调用。 */ + fun applyExtraTransforms(className: String, classBytes: ByteArray): ByteArray { + if (extraTransformers.isEmpty()) return classBytes + var cur = classBytes + for (t in extraTransformers) { + try { + val out = t(className, cur) + if (out != null) cur = out + } catch (e: Throwable) { + e.printStackTrace() + } + } + return cur + } + override fun map(internalName: String): String { return translate(internalName) } @@ -143,4 +174,4 @@ open class RemapTranslation : Remapper() { classBytes } } -} \ No newline at end of file +} diff --git a/module/incision/README.md b/module/incision/README.md new file mode 100644 index 000000000..cd91677df --- /dev/null +++ b/module/incision/README.md @@ -0,0 +1,547 @@ +# Incision + +Incision 是 TabooLib 的运行时织入模块,目标不是做一个通用 AOP/Mixin 框架,而是给 Bukkit/Paper/NMS 场景提供一套可控、可诊断、可回滚的手术式织入能力。 + +它同时支持两套入口: + +- DSL:`Scalpel { ... }` / `Scalpel.transient { ... }` +- 注解:`@Surgeon` + `@Lead/@Trail/@Splice/...` + +推荐策略: + +- 默认优先使用注解模式。 +- 只有在 patch 生命周期需要运行期动态控制时,才优先考虑 DSL。 +- 能稳定写成 `@Surgeon` 的长期 patch,不建议改成 DSL。 + +这份 README 同时包含: + +- 用户文档:怎么声明、怎么织入、怎么调试 +- 设计文档:模块分层、调度规则、字节码落点与约束 +- 术语对照表 +- 内部语法说明表 + +## 适用场景 + +适合: + +- 在 Bukkit/Paper 插件里做方法入口、出口、调用点级别的轻量织入 +- 对 NMS/Bukkit 方法做版本门控、remap 后再织入 +- 对 Kotlin `object`、`companion`、`@JvmStatic` 目标做双路径覆盖 +- 做临时 patch、范围 patch、线程局部 patch、批量 patch +- 对运行时问题提供可回滚的诊断性织入 + +默认选型: + +- 长期、稳定、随模块启动一起生效的 patch:优先 `@Surgeon` +- 临时、作用域、线程局部、事件驱动或诊断性 patch:再考虑 DSL + +不适合: + +- 把整套业务逻辑长期建立在大规模字节码改写之上 +- 期望它替代完整字节码框架或编译期 mixin 系统 +- 在完全不了解目标字节码形态时直接依赖复杂 `InsnPattern` + +## 生命周期边界 + +Incision 当前会尽量把注解式织入前推到 `LifeCycle.CONST`。 + +- `@Surgeon` 扫描与物理织入发生在 `CONST` +- `INIT / LOAD / ENABLE / ACTIVE` 的宿主行为理论上都可以被更早注册的 patch 命中 +- 但插件静态代码块,以及发生在 `ClassVisitorAwake(CONST)` 之前的更早引导行为,仍然不在可拦截范围内 + +换句话说,`ENABLE` 的确太晚;当前实现已经把可前移的部分推到了这个模块在 TabooLib 里的最前窗口。 + +## 快速上手 + +### 1. DSL 方式 + +这是补充能力,不是默认首选。 + +```kotlin +import taboolib.module.incision.annotation.SurgeryDesk +import taboolib.module.incision.api.Suture +import taboolib.module.incision.dsl.Scalpel as scalpel + +@SurgeryDesk +object DemoDesk { + + val greetPatch: Suture by scalpel { + lead("top.example.Target#greet(java.lang.String)java.lang.String") { theatre -> + println("before greet: ${theatre.args[0]}") + } + } + + fun patchOnce() { + scalpel.transient { + splice("top.example.Target#greet(java.lang.String)java.lang.String") { theatre -> + theatre.resume.proceed("patched") + } + }.use { + // 作用域内生效 + } + } +} +``` + +要点: + +- `scalpel {}` 只能放在 `@SurgeryDesk object` 内 +- 持久 patch 返回 `Suture` +- `transient/scoped/threadLocal/armOn/disarmOn/exclusive` 都受 `@SurgeryDesk` 调用点检查约束 +- 更适合临时 patch、诊断 patch 和动态启停场景,不适合作为常规长期 patch 的首选写法 + +### 2. 注解方式 + +这是默认推荐写法。 + +```kotlin +import taboolib.module.incision.annotation.Lead +import taboolib.module.incision.annotation.Operation +import taboolib.module.incision.annotation.Surgeon + +@Surgeon(priority = 50) +object DemoSurgeon { + + @Lead("method:top.example.Target#greet(java.lang.String)java.lang.String") + @Operation(id = "greet-lead", priority = 100) + fun beforeGreet(theatre: taboolib.module.incision.api.Theatre) { + println("before greet: ${theatre.args[0]}") + } +} +``` + +要点: + +- `@Surgeon` 只能标在 Kotlin `object` +- 扫描期会把方法翻译成 `AdviceEntry`,注册进 dispatcher,再触发织入 +- 方法级 `@Operation` 可以覆盖类级默认优先级和启用状态 +- 适合稳定、长期、声明式的 patch,是模块对外的首选模式 + +## Advice 类型总表 + +| 类型 | DSL/注解 | 典型用途 | 是否替换原逻辑 | 关键约束 | +| --- | --- | --- | --- | --- | +| `Lead` | `lead` / `@Lead` | 入口探针、计数、参数观察 | 否 | 只能表达入口语义 | +| `Trail` | `trail` / `@Trail` | 正常出口或异常出口收尾 | 否 | `onThrow=false` 时不覆盖异常出口 | +| `Splice` | `splice` / `@Splice` | 环绕、短路、改参与放行 | 可选 | 必须显式 `proceed/skip/override` | +| `Graft` | `graft` / `@Graft` | 在锚点前后追加逻辑 | 否 | 原指令仍会执行 | +| `Bypass` | `bypass` / `@Bypass` | 把单个调用点重定向到 handler | 是,替换单点 | 只替换目标位点,不替换整个方法 | +| `Trim` | `trim` / `@Trim` | 改写参数、返回值、局部变量 | 改值,不改流程 | 必须保证值类型兼容 | +| `Excise` | `excise` / `@Excise` | 整段方法覆写 | 是,替换整个方法 | 同一目标只能有一个 Excise | + +## Suture 生命周期 + +| 状态/动作 | 含义 | 典型接口 | +| --- | --- | --- | +| `ARMED` | 已织入并启用 | 初始成功状态 | +| `TRIGGERED` | 已触发过一次以上 | 运行统计态 | +| `SUSPENDED` | 字节码仍在,但 dispatcher 跳过 handler | `suspend()` | +| `HEALED` | 已卸载或回滚 | `heal()` / `close()` | +| `INACTIVE_UNRESOLVED` | 声明未成功解析 | 解析失败场景 | + +控制接口: + +- `heal()`:永久卸载 +- `suspend()`:临时停用,但不回滚织入点 +- `resume()`:恢复已挂起的 patch +- `close()`:等价于 `heal()` + +## DSL 模式 + +| 模式 | 作用 | 说明 | +| --- | --- | --- | +| `scalpel {}` | 持久 patch | 通常作为属性委托,返回 `Suture` | +| `scalpel.deferred {}` | 惰性 patch | 延迟到首次访问或目标类加载后 arm | +| `scalpel.transient {}` | 一次性 patch | 需手动 `heal` 或 `use` | +| `scalpel.scoped {}` | 作用域 patch | 块内生效,块外自动回收 | +| `scalpel.threadLocal {}` | 线程局部 patch | 默认不启用,按线程激活 | +| `scalpel.armOn/disarmOn` | 事件驱动 patch | 返回 `ArmTrigger`,由调用方决定何时 arm/disarm | +| `scalpel.exclusive` | 互斥 patch | 块内挂起同 target 的其他 ARMED patch | + +使用建议: + +- 如果 patch 可以在启动期静态声明,优先改写成注解模式。 +- 只有 patch 需要按代码路径即时创建、挂起、恢复或销毁时,再使用 DSL。 + +## 锚点与落点 + +| `Anchor` | 含义 | 常见用途 | +| --- | --- | --- | +| `HEAD` | 方法入口 | `Lead`、参数 Trim | +| `TAIL` | 正常出口前 | `Trail` 收尾 | +| `RETURN` | return 指令前 | 返回值 Trim | +| `INVOKE` | 方法调用处 | Graft/Bypass 调用点 | +| `FIELD_GET` | 字段读 | 字段读取探针 | +| `FIELD_PUT` | 字段写 | 字段写入探针 | +| `NEW` | `new` 指令 | 构造前后探针 | +| `THROW` | 抛异常处 | 异常路径观察 | + +`Site` 参数: + +| 字段 | 说明 | +| --- | --- | +| `anchor` | 锚点类型 | +| `target` | 锚点目标,例如 `owner#name(desc)ret` | +| `shift` | `BEFORE` / `AFTER` | +| `ordinal` | 第几个命中,`-1` 表示全部 | +| `offset` | 相对锚点再移动几条指令 | + +## 版本、remap、Kotlin 目标扩展 + +### `@Version` + +- 在扫描期决定某条 advice 是否注册 +- 默认 matcher 走 Minecraft/NMS 版本 +- 支持自定义 matcher FQCN +- matcher 解析失败会回退到 Noop matcher,这通常意味着该 advice 不会按预期筛选 + +### remap + +- 用户声明可以写逻辑上的 NMS owner/name/desc +- 运行时交给 `RemapRouter` / `TabooLibNmsResolver` 解析到实际类名 +- 安装 weaver 时会先做 owner 级映射,再对已加载类做 retransform + +### `@KotlinTarget` + +- 解决 Kotlin companion 实例方法与 `@JvmStatic` 静态桥接方法是两条调用路径的问题 +- 可分别扩展到: + - `companionInstance` + - `jvmStaticBridge` + +## 诊断与排错 + +优先看三类信息: + +- `Forensics.debug/warn` +- `Trauma.*` +- `Incision-Test` 对应分类用例 + +常见问题: + +| 现象 | 常见原因 | 先看哪里 | +| --- | --- | --- | +| advice 未命中 | 描述符错、scope 过宽或过窄、pattern 不匹配 | `DescriptorCodec`、`Scope`、`InsnPattern` | +| `ResumeMissing` | `Splice` 没有显式放行或短路 | handler 本身 | +| 只命中 Java,不命中 Kotlin | 漏了 companion / `@JvmStatic` 扩展 | `@KotlinTarget` | +| 某些 NMS 版本不生效 | 版本过滤或 remap 结果不一致 | `@Version`、`RemapRouter` | +| 同 target 顺序不对 | 优先级或注册顺序认知错误 | `AdviceChain` 排序规则 | +| 运行时写死 dispatcher 类名失效 | 该类在运行时会被重定向 | 不要硬编码 runtime redirect 目标 | + +## 术语对照表 + +| 术语 | 对外理解 | 代码对应 | +| --- | --- | --- | +| 手术 / patch | 一组对目标方法生效的织入声明 | `Suture` | +| 施术者 | 持有注解式 advice 的 `object` | `@Surgeon` | +| 工作台 | 持有 DSL patch 的 `object` | `@SurgeryDesk` | +| 现场 | advice 执行时看到的上下文 | `Theatre` | +| 放行 | 继续执行原方法或原指令 | `resume.proceed()` | +| 短路 | 不再执行原方法,直接给结果 | `resume.skip()` / `override()` | +| 锚点 | 要插入或替换的字节码位置 | `Anchor` / `Site` | +| 链 | 某个 target 下的 advice 顺序集合 | `AdviceChain` | +| 调度器 | 运行时按 target 分发 advice 的中心 | `TheatreDispatcher` | +| 织入器 | 把 dispatcher 调用写回字节码的组件 | `Scalpel.installWeaver` / `SiteWeaver` | + +## 内部语法说明表 + +### 1. 方法描述符 + +Incision 内部统一使用: + +```text +owner#method(arg1,arg2,...)returnType +``` + +示例: + +| 写法 | 含义 | +| --- | --- | +| `org.bukkit.entity.Player#kickPlayer(java.lang.String)void` | 实例方法 | +| `top.example.Target$Companion#echo(java.lang.String)java.lang.String` | Kotlin companion 实例方法 | +| `net.minecraft.server.MinecraftServer#getPlayerCount()int` | NMS 方法 | + +说明: + +- `owner` 最终会转成 JVM internal name +- Kotlin companion 与 `@JvmStatic` 可能需要额外 target 扩展 +- 描述符错误通常会落到 `Trauma.Declaration.BadDescriptor` + +### 2. Scope DSL + +| 语法 | 含义 | +| --- | --- | +| `class:com.foo.Bar` | 匹配类 | +| `method:Foo#bar(*)` | 匹配方法 | +| `field:Foo#name:String` | 匹配字段 | +| `&` | 与 | +| `|` | 或 | +| `!` | 非 | + +示例: + +```text +class:org.bukkit.entity.Player & method:org.bukkit.entity.Player#kickPlayer(*) +``` + +### 3. `InsnPattern` / `Step` + +| 字段 | 说明 | +| --- | --- | +| `opcode` | 目标 opcode,`Op.ANY` 表示任意 | +| `owner` | 调用/字段所属类,支持 glob | +| `name` | 方法名或字段名,支持 glob | +| `desc` | 方法描述符或字段类型,支持 glob | +| `cst` | 常量值约束 | +| `repeat` | 连续重复次数 | + +说明: + +- 它匹配的是编译后字节码,不是源码 +- 常量折叠、编译器优化、Kotlin 桥接方法都会影响结果 +- 当前测试矩阵已覆盖 `ICONST/LDC/NEW/INVOKE/GOTO/ARRAYLENGTH/PUTFIELD/...` + +### 4. `where` 谓词 + +`where` 适合做命中后的二次筛选,不替代描述符和锚点。 + +已覆盖能力: + +| 能力 | 示例 | +| --- | --- | +| 字面量 | `true` / `false` / `null` / `1.5` | +| 比较 | `x == "a"`、`n > 3`、`n <= 10` | +| 集合 | `x in ["a","b"]` | +| 类型 | `x is java.lang.String` | +| 属性/方法 | `self.name`、`arg0.length()` | +| 逻辑 | `a && b`、`a || b`、`!(a)` | + +建议: + +- 先用 descriptor / scope 缩小范围,再用 `where` +- `where` 写复杂时优先加对应测试用例,不要只靠肉眼判断 + +## 设计文档 + +### 分层 + +| 层 | 责任 | 关键文件 | +| --- | --- | --- | +| 声明层 | DSL 与注解声明 | `dsl/Scalpel.kt`、`annotation/*` | +| 扫描层 | 把 `@Surgeon` 方法翻译成运行时条目 | `loader/SurgeonScanner.kt` | +| 注册层 | 维护 patch 生命周期与 target 索引 | `runtime/SurgeryRegistry.kt`、`api/Suture.kt` | +| 调度层 | 在方法命中时执行 advice 链 | `runtime/TheatreDispatcher.kt` | +| 落点层 | 负责 `SiteSpec`、pattern、offset 等落点计算 | `weaver/site/*` | +| 织入层 | 对目标类做 retransform 并写入 dispatcher 调用 | `weaver/*`、`loader/*Backend*` | +| 诊断层 | 暴露 warn/debug/Trauma | `diagnostic/*` | + +### 运行流程 + +1. 用户通过 DSL 或 `@Surgeon` 声明 advice。 +2. 声明被翻译成 `AdviceEntry`。 +3. `AdviceEntry` 注册到 `TheatreDispatcher`。 +4. `Scalpel.installWeaver` 按 owner 聚合目标并触发 retransform。 +5. 织入器把 dispatcher 调用写进目标字节码。 +6. 运行期命中目标方法时,dispatcher 按 target 拉出 `AdviceChain` 执行。 +7. `Suture` 负责这组 advice 的启停、挂起和卸载。 + +### 排序规则 + +- 按 `priority` 降序 +- 同优先级保持注册顺序 +- 类级 `@Surgeon(priority)` 是默认值 +- 方法级 `@Operation(priority)` 可覆盖类级默认值 + +### 关键设计约束 + +#### 1. 先 dispatcher,再 handler + +Incision 不把业务 handler 直接写进字节码,而是只写 dispatcher 调用。这样能保证: + +- 运行时可以统一做 enable/disable/suspend/resume +- advice 注册与卸载可集中管理 +- 诊断入口稳定 + +#### 2. 先 owner 聚合,再 retransform + +同一个 owner 上的 advice 会先聚合后再织入,避免重复 retransform 带来额外抖动。 + +#### 3. classloader 与 remap 必须一起考虑 + +这个模块天然有两个不稳定维度: + +- classloader +- NMS/Bukkit remap + +因此: + +- 不要直接硬编码可能被运行时重定向的类名 +- 平台可直连时优先 import 平台 API,再由平台启用条件决定是否生效 +- 运行时 owner 以 `RemapRouter.resolveOwner` 结果为准 + +#### 4. `InsnPattern` 是能力,不是默认路径 + +字节码序列匹配最强,但也最脆。能用明确描述符和 `Site` 解决的问题,不应先上复杂 pattern。 + +### 与测试矩阵的对应关系 + +| 设计面 | 主要测试分类 | +| --- | --- | +| DSL 生命周期 | 基础 DSL | +| 注解扫描与翻译 | Surgeon 注解 | +| Kotlin companion / `@JvmStatic` / operation 元信息 | 元信息与 Kotlin | +| 版本门控 / remap / Bukkit / NMS / 跨 CL | 平台与版本 | +| 谓词 / opcode 序列 / offset | 谓词与字节码 | +| anchor / site / trim / trauma 诊断 | 锚点矩阵与诊断 | + +## 访问字段与方法 + +Handler 内可以读写目标类(或任意其他类)的 private / final / static 字段,以及调用 private 方法。底层走 JVMTI JNI,完全绕过 Java 访问控制,不依赖 `setAccessible`,不受 JDK 17+ 模块封装影响。 + +### 推荐写法:Lambda 工厂(类级声明) + +```kotlin +import taboolib.module.incision.api.* +import taboolib.module.incision.annotation.Surgeon +import taboolib.module.incision.annotation.Lead + +@Surgeon +object AsyncTeleportPatch { + + // 类级声明 — 解析一次,处处复用 + private val teleportOwner = field("teleportOwner") + private val parentFuture = field(AsyncTimedTeleport::class.java, "parentFuture") + private val moveConstant = staticField(AsyncTimedTeleport::class.java, "MOVE_CONSTANT") + private val setRespawn = fieldSet(AsyncTimedTeleport::class.java, "timer_respawn") + private val doCheck = method("checkPermission") + + @Lead("net/ess3/api/v2/services/Teleport#cooldown(Z)V") + fun cooldown(t: Theatre) { + val owner = teleportOwner(t) // 读 private final 实例字段 + val limit = moveConstant() // 读 private static final 常量 + setRespawn(t, true) // 写 private final 实例字段 + val ok = doCheck(t, "essentials.tp") // 调用 private 方法 + } +} +``` + +工厂类型一览: + +| 工厂函数 | 返回类型 | 调用形式 | +| --- | --- | --- | +| `field(name)` | `FieldAccessor` | `accessor(theatre)` 或 `accessor(receiver)` | +| `field(ownerClass, name)` | `FieldAccessor` | 同上,指定声明类 | +| `staticField(ownerClass, name)` | `StaticFieldAccessor` | `accessor()` | +| `fieldSet(name)` | `FieldSetter` | `setter(theatre, value)` | +| `fieldSet(ownerClass, name)` | `FieldSetter` | 同上 | +| `staticFieldSet(ownerClass, name)` | `StaticFieldSetter` | `setter(value)` | +| `method(name, descriptor?)` | `MethodAccessor` | `accessor(theatre, arg1, arg2)` | +| `staticMethod(ownerClass, name, descriptor?)` | `StaticMethodAccessor` | `accessor(arg1, arg2)` | + +### 辅助写法:Theatre 直接调用 + +适用于一次性、不值得声明 val 的场景: + +```kotlin +@Lead("...") +fun handler(t: Theatre) { + val name: String? = t.field("playerName") + val count: Int? = t.staticField(SomeClass::class.java, "MAX_COUNT") + t.setField("enabled", false) + t.invoke("notifyAll") +} +``` + +### 通用工具扩展 + +以下顶层扩展函数可在任何地方使用,不限于 `Theatre` 作用域: + +```kotlin +import taboolib.module.incision.api.* + +// 安全转型 +val greetable: Greetable? = someObject.cast() + +// 强制转型(失败抛 ClassCastException) +val str: String = someObject.castOrThrow() + +// 读任意对象的字段(private / final 均可,沿继承链自动解析) +val secret: String? = someObject.readField("secret") + +// 写任意对象的字段 +someObject.writeField("secret", "modified") + +// 调用任意对象的方法(private 均可,按名称 + 参数类型匹配) +val result: String? = someObject.callMethod("greet") +``` + +Theatre 上还有参数便捷方法: + +```kotlin +@Lead("...") +fun handler(t: Theatre) { + val name: String? = t.arg(0) // 越界返回 null + val id: Int = t.argOrThrow(1) // 越界抛 IndexOutOfBoundsException + val self: MyClass? = t.selfAs() // self 安全转型 +} +``` + +实战示例(Essentials /list 命令钩子): + +```kotlin +@Surgeon +object EssentialsListHook { + + private const val TARGET = "method:com.earth2me.essentials.commands.Commandlist#run(*)" + + private fun getSender(theatre: Theatre): CommandSender? { + val source = theatre.arg(1) ?: return null + return source.callMethod("getSender") + } + + @Lead(scope = TARGET) + fun beforeList(theatre: Theatre) { + getSender(theatre)?.sendMessage("§a[Incision] 即将执行 /list 命令...") + } + + @Trail(scope = TARGET) + fun afterList(theatre: Theatre) { + getSender(theatre)?.sendMessage("§a[Incision] /list 命令执行完毕!") + } +} +``` + +### 注意事项 + +- 修改 `static final` 原始类型或 String 字段可能不会对已 JIT 过的调用点生效(常量折叠)。实例 final 字段不受影响。 +- JVMTI 不可用时自动降级到反射 + Unsafe,但 JDK 17+ 非开放模块的 private 字段可能降级失败。 +- 方法重载场景下,如果按参数类型匹配到多个候选,需要显式传入 `descriptor` 参数。 + +## 选型建议 + +优先级顺序: + +1. 先判断能否写成 `@Surgeon` + 注解 +2. 只有在生命周期必须动态化时,才退到 DSL + +优先选注解模式的典型场景: + +- 插件启动后就应长期存在的 patch +- 可以稳定写死 target/scope/site 的 patch +- 希望声明和行为集中在一个 `object` 中,便于扫描、排序和维护 + +适合用 DSL 的典型场景: + +- `transient` 一次性 patch +- `scoped` 块级 patch +- `threadLocal` 线程级 patch +- `armOn/disarmOn` 事件驱动 patch +- 调试、压测、临时诊断和人工操作型 patch + +## 维护建议 + +- 修改 DSL 或注解语义时,同时更新: + - 本 README + - `annotation/*` KDoc + - `Incision-Test` 对应用例与 `CaseDocs` +- 修改 bytecode matching、offset、anchor 逻辑时,优先跑对应矩阵,而不是只看单个 case +- 看到运行时 warning 时,先判断它是不是测试刻意覆盖的 fallback 样本,再决定是否修复 diff --git a/module/incision/TECHNICAL.md b/module/incision/TECHNICAL.md new file mode 100644 index 000000000..2c9a9a3ec --- /dev/null +++ b/module/incision/TECHNICAL.md @@ -0,0 +1,581 @@ +# Incision 技术原理 + +本文档用于解释 Incision 模块的底层实现原理,面向需要深入理解或二次开发的读者。 +用户向导请看 [README.md](README.md)。 + +--- + +## 目录 + +1. [一句话定性](#1-一句话定性) +2. [核心替换链路](#2-核心替换链路) +3. [IncisionBridge 桥模式](#3-incisionbridge-桥模式) +4. [三个后端](#4-三个后端) +5. [JvmtiBackend 深入剖析](#5-jvmtibackend-深入剖析) +6. [两种独立的"编译"](#6-两种独立的编译) +7. [FrameVerifier 安全网](#7-frameverifier-安全网) +8. [生命周期与织入时机](#8-生命周期与织入时机) +9. [与其他 AOP 技术对比](#9-与其他-aop-技术对比) +10. [限制与权衡](#10-限制与权衡) +11. [FAQ](#11-faq) + +--- + +## 1. 一句话定性 + +**运行时字节码织入(Runtime Bytecode Weaving)**,不是动态代理,不修改类名。 + +| 问题 | 答案 | +|---|---| +| 是动态代理吗? | 不是。JDK Proxy 会生成 `$Proxy0` 新类并且需要接口;Incision 不创建新类 | +| 会修改原本类的类名吗? | **不会**。类名、`Class` 对象、ClassLoader、已有实例引用全部保留 | +| 那改了什么? | 只改目标方法**方法体内部**的字节码,插入一条固定的 `INVOKESTATIC` 调用 | +| 怎么让修改生效? | 通过 `java.lang.instrument.Instrumentation.retransformClasses()` 或 JVMTI 原生 `RetransformClasses` 对已加载的类做原地热替换 | + +关键认知:**JVM 不允许"重新加载"已加载的类**,但允许**原地替换方法体**。Incision 利用的就是这个能力。 + +--- + +## 2. 核心替换链路 + +### 2.1 总体流程 + +``` +┌───────────────────────────────────────────────────────────┐ +│ 用户声明:@Surgeon / Scalpel {} DSL │ +└───────────────────────────────────────────────────────────┘ + │ CONST 阶段 + ▼ +┌───────────────────────────────────────────────────────────┐ +│ SurgeonScanner 扫描 → AdviceEntry │ +│ TheatreDispatcher 注册 → target-signature 索引 │ +└───────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ Scalpel 按 owner 聚合目标,触发 Backend.retransform │ +└───────────────────────────────────────────────────────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + Instrumentation JVMTI ClassLoaderHook + │ │ │ + └──────────────┼──────────────┘ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ transform 回调:原字节码 → Scalpel.weave → 新字节码 │ +│ ├─ ClassNode + ASM Tree API │ +│ ├─ AdviceAdapter 在 HEAD/TAIL/RETURN 插入 dispatcher 调用│ +│ ├─ SiteWeaver 在 INVOKE/FIELD/NEW 等锚点插入 │ +│ ├─ FrameVerifier 预检帧一致性,失败回退原字节码 │ +│ └─ ClassWriter(COMPUTE_FRAMES) 写出 │ +└───────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────────────────────────────────────────┐ +│ JVM 接受新字节码,原地替换方法体 │ +│ 类名 / Class 对象 / 父类 / 接口 / 字段 / 方法签名全保留 │ +│ 已有实例继续有效,下次调用命中新字节码 │ +└───────────────────────────────────────────────────────────┘ +``` + +### 2.2 插入的字节码形态 + +weaver 在目标方法体的关键位点插入**一条固定调用**: + +``` +INVOKESTATIC io/izzel/incision/bridge/IncisionBridge.dispatch + (Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; +``` + +参数含义: + +| 参数 | 内容 | +|---|---| +| `String targetSignature` | 目标方法签名(编译期常量串,带 phase 后缀如 `@TRAIL_THROW`) | +| `Object self` | 实例方法的 `this`;静态方法为 `null` | +| `Object[] args` | 原方法的所有实参装箱后的数组 | +| 返回值 | advice 链决定的结果,`null` 表示"继续执行原方法" | + +**字节码里不内联 handler 逻辑**——只塞一个桥调用。真正的 handler 方法注册在运行时的 `SurgeryRegistry` 里,由 `TheatreDispatcher` 按 signature 查 `AdviceChain` 执行。 + +这种"只写桥"的设计带来三个好处: + +1. **运行时可 suspend/resume/heal**:字节码只织一次,启停靠 dispatcher 层旁路 +2. **handler 可热插拔**:增删 advice 不需要重新 retransform +3. **诊断入口统一**:所有织入都经过同一个 dispatch 路径,便于监控和故障分析 + +--- + +## 3. IncisionBridge 桥模式 + +### 3.1 为什么需要这个桥 + +如果 weaver 直接把调用写成 `INVOKESTATIC taboolib/module/incision/runtime/TheatreDispatcher.dispatch`,会遇到一个致命问题: + +**TabooLib Gradle 插件的 `RelocateRemapper` 在每个插件打包时会把 `taboolib.*` 重定向为 `.taboolib.*`**。 + +结果是: +- 插件 A 里的 `TheatreDispatcher` 被重定向为 `com.a.taboolib.module.incision.runtime.TheatreDispatcher` +- 插件 B 里的 `TheatreDispatcher` 被重定向为 `com.b.taboolib.module.incision.runtime.TheatreDispatcher` +- 两者类名不同、ClassLoader 不同,**无法共享同一个 dispatcher** +- 插件 A 织入的方法里写死了插件 A 的 `TheatreDispatcher`,插件 B 无法拦截同一 target + +### 3.2 桥的位置 + +桥必须放在**不被 relocate 的包名下**,且用 **Java 实现**避免 Kotlin 运行时依赖(`kotlin.*` 同样会被 relocate)。 + +```java +// module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java +package io.izzel.incision.bridge; + +public final class IncisionBridge { + public static Object dispatch(String targetSignature, Object self, Object[] args) { + // 1. 优先走系统 ClassLoader 上的 IncisionGateHost(多插件共享宿主) + // 2. 退化为当前调用方 ClassLoader 的本地 TheatreDispatcher(单插件场景) + } +} +``` + +`io.izzel.*` 在 relocate 名单之外,所以所有插件的织入字节码都指向**同一个**桥类。 + +### 3.3 双路径解析 + +``` +织入字节码 + │ + ▼ + IncisionBridge.dispatch + │ + ├─► 路径 A:系统 ClassLoader 上的 IncisionGateHost + │ (由 GateBootstrapper 通过 appendToSystemClassLoaderSearch 推入) + │ 所有插件共享同一宿主,advice 跨插件可见 + │ + └─► 路径 B:ClassLoader-local 的 TheatreDispatcher + (fallback,单插件场景或宿主绑定失败时) + 仅当前插件 ClassLoader 内部可见 +``` + +路径 A 是正式路径,路径 B 是兜底。两条都通过反射穿透 ClassLoader 边界,避免类型一致性问题(不同 CL 的 `TheatreDispatcher` 虽然同名但不是同一个 `Class`)。 + +--- + +## 4. 三个后端 + +```kotlin +interface Backend { + val name: String + fun available(): Boolean + fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): BackendToken + fun retransform(className: String): Boolean +} +``` + +三个实现各有定位: + +| 后端 | 原理 | 已加载类 | 未加载类 | 生产环境可用性 | +|---|---|---|---|---| +| **InstrumentationBackend** | self-attach → `java.lang.instrument.Instrumentation.retransformClasses` | ✅ | ✅ | 受限(JDK 21+ 需开关,Paper 常禁) | +| **JvmtiBackend** | 预编译 native agent → JVMTI `RetransformClasses` | ✅ | ✅ | **最稳**(不依赖 attach 权限) | +| **ClassLoaderHookBackend** | 委派给 InstrumentationBackend,本身是占位 | ❌ | ✅ | 依附前两者 | + +### 4.1 InstrumentationBackend + +通过 self-attach 拿到 `Instrumentation` 句柄: + +```kotlin +private fun resolve(): Instrumentation? { + return ByteBuddyAttacher.tryInstall() // 优先:需要 byte-buddy-agent + ?: ManualSelfAttach.attach() // 兜底:JDK 8 用 tools.jar,JDK 9+ 用 jdk.attach 模块 +} +``` + +**self-attach 的固有阻碍**: + +- JDK 9+:默认 `jdk.attach.allowAttachSelf=false`,需显式打开 +- JDK 21+:无 `-XX:+EnableDynamicAgentLoading` 时打印警告并最终禁止 +- Paper/Spigot 生产配置:常以安全硬化名义禁用 self-attach +- 精简 JRE:可能缺 `tools.jar` 或 `jdk.attach` 模块 + +一旦任一条件未满足,Instrumentation 路径直接不可用。 + +### 4.2 JvmtiBackend + +详见下一节。 + +### 4.3 ClassLoaderHookBackend + +名字具有迷惑性——它并不真的 hook ClassLoader。实现里: + +```kotlin +// 安装时直接把请求委派给 InstrumentationBackend +for ((cls, list) in transformers) { + for (t in list) InstrumentationBackend.addTransformer(cls, t) +} +``` + +**实际作用**:保留一个抽象层便于未来引入真正的 ClassLoader 级拦截(比如对无法 retransform 的引导类),目前是 Instrumentation 的别名。 + +--- + +## 5. JvmtiBackend 深入剖析 + +### 5.1 存在的核心理由 + +绕开 self-attach 的封锁。 + +InstrumentationBackend 依赖 self-attach,而 self-attach 在真实服务端环境里经常堵死(见 4.1)。JvmtiBackend 把路径换成: + +**不做 attach,直接 `System.load()` 加载一个自己写的 JVMTI native agent。** + +`System.load()` 是 JNI 标准能力,几乎任何 JVM 都不会拦——比 attach 权限宽松得多。 + +### 5.2 物理组成 + +``` +module/incision/src/main/c/ + ├─ incision_jvmti.c ← 手写 C 源码 + ├─ build-all.bat ← 交叉编译脚本 + └─ include/ ← JNI 头文件 + +module/incision/src/main/resources/native/ + ├─ windows/x64/incision-jvmti.dll + ├─ windows/arm64/incision-jvmti.dll + ├─ linux/x64/libincision-jvmti.so + ├─ linux/arm64/libincision-jvmti.so + ├─ macos/x64/libincision-jvmti.dylib + └─ macos/arm64/libincision-jvmti.dylib +``` + +**预编译 6 个平台二进制**打进 jar,用户不需要装编译工具链。 + +### 5.3 自举流程 + +```kotlin +fun tryLoad(): Boolean { + val lib = extractNativeLib() ?: return false + // 关键:在 System.load 之前设置 property + System.setProperty("incision.jvmti.class", JvmtiBackend::class.java.name) + System.load(lib.absolutePath) + return nInit(JvmtiBackend::class.java) +} +``` + +步骤: + +1. 检测 `os.name` / `os.arch` → 选对应 .dll/.so/.dylib +2. 释放到 `java.io.tmpdir/incision-native/`(文件名带哈希,去重复用) +3. 设置 system property `incision.jvmti.class` 指向当前 JvmtiBackend 的**实际全限定名** +4. `System.load(lib)` → native 的 `JNI_OnLoad` 回调 +5. native 从 property 读类名 → `FindClass` → `RegisterNatives` 绑定 JNI 方法 +6. `nInit` 里 native 调 JVMTI `AddCapabilities` 获取 `can_retransform_classes` / `can_redefine_classes` / `can_generate_all_class_hook_events` 等能力 +7. 挂 `ClassFileLoadHook` 事件,bytes 经过它时回调 Java 侧 `onClassFileLoad` + +### 5.4 为什么用 system property 传类名 + +`TabooLib Gradle 插件` 的 `RelocateRemapper` 会把 `taboolib.module.incision.loader.JvmtiBackend` 重定向为 `.taboolib.module.incision.loader.JvmtiBackend`。 + +native 代码里**不能写死 Java 类名**,否则重定位后找不到。用 property 动态传名字是这里的标准解法。 + +### 5.5 五项职责 + +JvmtiBackend 不只是"另一个 retransform 通道",它提供了其他后端没有的独家能力: + +#### 职责 1:retransform 主执行器 + +```kotlin +override fun retransform(className: String): Boolean { + val cls = findLoadedClass(className) ?: return true // 未加载 → 靠 ClassFileLoadHook 命中 + return nRetransform(cls) // 已加载 → JVMTI RetransformClasses +} +``` + +native 侧调 `jvmtiEnv->RetransformClasses(env, 1, &target)`。与 Instrumentation 走的是同一套 JVM 底层 API,只是不经过 `java.lang.instrument` 壳。 + +#### 职责 2:原始字节码缓存 + +```kotlin +// Scalpel.weave 首句 +val sourceBytes = JvmtiBackend.getCachedOriginal(probeOwner) ?: run { + JvmtiBackend.cacheOriginal(probeOwner, originalBytes) + originalBytes +} +``` + +**为什么关键**:一个类被织入后,下次 retransform 时 JVM 喂过来的 bytes 是**已经被上次修改过的**。基于它再织会叠层。 + +native 侧缓存**首次见到的干净字节码**,任何重织都从原始状态出发,保证幂等。Instrumentation 根本没有这个能力。 + +#### 职责 3:原始字节码抽取 `nExtractClassBytes` + +对**从未织入过**的已加载类,通过 `RetransformClasses` + ClassFileLoadHook 的特殊组合直接捞 bytes。用于诊断、签名校验、冲突分析等场景。 + +Instrumentation 无法做这件事——它只在 transform 回调里短暂看得见 bytes,回调结束就丢了。 + +#### 职责 4:JNI 级 `nDefineClass` + +```kotlin +fun defineClassInClassLoader(loader: ClassLoader?, name: String, bytes: ByteArray): Class<*>? +``` + +JNI 的 `DefineClass` 可以往**任意 ClassLoader**(包括 bootstrap、ext、system)塞类,不需要那个 ClassLoader 配合: + +- 不要求 ClassLoader 是 `URLClassLoader` +- 不要求反射调 `defineClass`(受 `accessible` 限制) +- 不要求 ClassLoader 有公开 API + +用途:`BodiesClassGenerator` 生成的桥接类、`PredCompiler` 的谓词类——这些需要精准落户到某个 CL 的辅助类,走 JNI 最干净。 + +#### 职责 5:绕过访问控制的字段/方法访问器 + +```kotlin +external fun nFieldGet(obj, ownerClass, fieldName, fieldDesc): Any? +external fun nFieldSet(...) +external fun nStaticFieldGet(...) +external fun nStaticFieldSet(...) +external fun nInvokeMethod(...) +``` + +JNI 从 VM 底层操作字段和方法,**完全绕过** Java 的 `IllegalAccessException`、模块封装、`setAccessible` 警告。 + +- 不需要 `Field.setAccessible(true)`(JDK 17+ 会警告或拒绝) +- 不受模块边界约束(`Unsafe` 也做不到这一点的一部分) +- 不受 `Lookup` 的私有访问限制 + +这是 Reflex / `Unsafe` 都做不到或做得不干净的事。Scalpel 和 Surgeon 需要读写目标对象私有状态、调用 private/package-private 方法时不必到处喷 `setAccessible`。 + +**用户侧出口**:`IncisionAccessor`(`api/IncisionAccessor.kt`)是这些 native 方法的唯一用户层门面。它提供解析缓存(`(Class, fieldName) → ResolvedField`)和三级 fallback(JVMTI → 反射 → Unsafe)。handler 通过 Lambda 工厂(`api/Accessors.kt`)或 `Theatre` 接口的 default 方法间接调用 `IncisionAccessor`,不直接接触 `JvmtiBackend`。 + +### 5.6 在系统中的位置 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 织入请求 │ +└──────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │ Backend 选择(按 available) │ + └──────────────────────────┘ + │ │ + ┌───────────▼──────────┐ ┌─────▼─────────────────┐ + │ InstrumentationBE │ │ JvmtiBackend │ + │ (self-attach) │ │ (native agent) │ + │ │ │ │ + │ • 开发机常用 │ │ • 生产主力 │ + │ • 有 attach 权限即可 │ │ • 无视 attach 管制 │ + │ • 仅 retransform │ │ • + 原字节码缓存 │ + │ │ │ • + 原始 bytes 抽取 │ + │ │ │ • + 任意 CL defineClass│ + │ │ │ • + 无访问控制字段/方法│ + └──────────────────────┘ └───────────────────────┘ +``` + +### 5.7 一句话定位 + +**JvmtiBackend 是"无视管制、能力更强、带缓存"的 native 兜底主力——它让 Incision 在 Paper/JDK 21+ 生产服务器上真正能跑,同时提供其他后端根本没有的原字节码缓存、裸 defineClass、无访问控制的字段/方法访问这些独家能力。** + +--- + +## 6. 两种独立的"编译" + +Incision 内部有两条**完全不同**的字节码生产链路,初看容易混淆: + +| | **核心织入(Scalpel / SiteWeaver)** | **PredCompiler** | +|---|---|---| +| 目标 | 改已存在的用户类(Bukkit / NMS / 插件自己的类) | 编译 `where "..."` 这种 DSL 字符串 | +| 产出 | 原类的修改版字节码 | 一个全新的 `Predicate` 实现类 | +| 类名 | **保持原名**(`org.bukkit.entity.Player` 还是 `Player`) | 生成新名(`.../pred/gen/Pred$0`, `$1`...) | +| 加载方式 | `retransformClasses` 原地替换 | `defineClass` 加载新类 | +| 什么时候发生 | CONST 阶段、insertion 时 | advice 注册时一次性 | +| 运行时开销 | 一次性写入,后续调用直接走新字节码 | `Predicate.test(ctx)` 像普通方法调用 | + +两者都用 ASM,但机制和目的完全不同——**本体是改现有类,PredCompiler 是造新类**。 + +`where` 为什么要编译成类?因为谓词在运行时被高频调用(每次方法命中都评估),用反射或解释器会炸掉性能。编译成独立 `Predicate` 子类后,`dispatcher.pred.test(ctx)` 就是普通虚方法调用,JIT 能充分优化。 + +--- + +## 7. FrameVerifier 安全网 + +Tree API 改写 InsnList 后,JVM 加载时会跑一次字节码校验(Verifier)。任何 stack map frame 不一致都会抛 `VerifyError`,而且错误信息很难定位——直接 crash 服务器。 + +Incision 在写出新字节码**之前**先自己跑一遍 `Analyzer` + `BasicVerifier`: + +```kotlin +// Scalpel.applySiteWeaver +try { + FrameVerifier.verify(classNode) +} catch (e: AnalyzerException) { + Forensics.warn("帧验证失败,回退原字节码: $className.$methodName — ${e.message}") + return bytes // 返回织入前的字节码 +} +``` + +**失败就回退**,不把坏 class 喂给 JVM。这是"开发期字节码 bug 导致服务器崩溃"这类事故的最后一道防线。 + +代价:每次织入多一次完整字节码分析,但只在 CONST 启动期,运行时零开销。 + +--- + +## 8. 生命周期与织入时机 + +Incision 把 `@Surgeon` 扫描和物理织入尽量前推: + +``` +NONE → CONST → INIT → LOAD → ENABLE → ACTIVE → DISABLE + │ + └─► @Surgeon 扫描 + Scalpel 织入 发生在这里 +``` + +**为什么是 CONST 而不是 ENABLE**: + +- `@Awake(LifeCycle.ENABLE)` 里注册的 advice 来不及拦截 `@Awake(INIT/LOAD)` 里的代码 +- `CONST` 是 TabooLib 给用户代码开放的最早窗口 +- 更早的阶段(`ClassVisitorAwake` 之前的 TabooLib 内部引导)无法干预 + +**仍然不能拦截的**: + +- 插件 main class 的静态初始化块(`` 发生在类加载瞬间,比 CONST 更早) +- TabooLib 自身启动阶段的代码(织入器还没启动) +- bootstrap / system ClassLoader 上某些核心类(取决于 retransformable 能力) + +对于"确实需要更早"的极端场景,用户需要通过 ClassFileTransformer 机制在 pre-main 阶段注册(Incision 不自动代劳)。 + +--- + +## 9. 与其他 AOP 技术对比 + +| 特性 | Incision | JDK Proxy | cglib | Mixin | AspectJ LTW | +|---|---|---|---|---|---| +| 改类名? | ❌ | ✅ 生成 `$Proxy0` | ✅ 生成 `$$EnhancerByCGLIB` | ❌ | ❌ | +| 需接口? | ❌ | ✅ | ❌ | ❌ | ❌ | +| 拦 final 方法? | ✅ | ❌ | ❌(final 类/方法拦不了) | ✅ | ✅ | +| 拦已加载类? | ✅ | ❌(必须通过代理对象调用) | ❌ | 需 launch wrapper | ✅ | +| 拦 NMS 内部 `this.foo()`? | ✅ | ❌(自调用拦不住) | ❌ | ✅ | ✅ | +| 需编译期 agent? | ❌ | ❌ | ❌ | ✅ 启动参数 | ✅ 启动参数 | +| 热插拔? | ✅(dispatcher 层 suspend/resume) | ❌ | ❌ | ❌ | ❌ | +| 在 Paper 正式服跑? | ✅(JVMTI 后端) | ✅ | ✅ | 看 launcher | 依赖 agent | +| 语法复杂度 | 中(注解 + DSL) | 低 | 低 | 中(Java mixin) | 高(AspectJ 语法) | + +**核心差异化**: + +- 对 JDK Proxy / cglib:Incision 解决的是"目标已经在那里、不走代理对象的调用路径"问题。NMS / Bukkit 的代码就是这种场景。 +- 对 Mixin:Mixin 需要 launch wrapper(Minecraft 客户端场景常用),而 Incision 是纯运行时插件级,不改启动链路。 +- 对 AspectJ LTW:AspectJ 功能最全但需要 `-javaagent` 参数。Incision 的 JVMTI 后端实现了"**无任何启动参数的 retransform**"。 + +--- + +## 10. 限制与权衡 + +### 10.1 技术限制(来自 JVM) + +`retransformClasses` 和 JVMTI `RetransformClasses` 的约束: + +| 项 | 能改 | 不能改 | +|---|---|---| +| 类名 | | ❌ | +| 父类 / 接口 | | ❌ | +| 方法签名(增删方法、改参数) | | ❌ | +| 字段(增删字段、改类型) | | ❌ | +| 方法体字节码 | ✅ | | +| 注解 | 部分 JVM 支持 | | + +**推论**:Incision 不能做 Mixin 的 `@Shadow` 新字段、不能加新方法。只能改方法体——这也是为什么所有插入都以 `dispatch` 调用的形式出现。 + +### 10.2 性能特征 + +- **启动期**:每个织入目标多一次 retransform + ASM 解析 + FrameVerifier 分析 +- **运行期**:每次命中目标方法多一次 `IncisionBridge.dispatch` → 反射 invoke → 查表 → 执行 handler +- **反射开销**:跨 ClassLoader 的桥调用用反射,但热点会被 JIT inline;实测开销在微秒量级 + +不建议在每秒百万调用的热路径上织入。适合事件入口、网络包、命令处理这类"逻辑决策点"。 + +### 10.3 跨 ClassLoader 复杂性 + +Minecraft 插件体系的 ClassLoader 拓扑本就复杂(PluginClassLoader / IsolatedClassLoader / AppClassLoader / Bootstrap),再叠加 TabooLib 的包重定位,使得"同一个类名"在不同 CL 下是**不同的 Class 对象**。 + +Incision 的应对: +- 桥放在不被重定位的 `io.izzel.*` 包 +- 跨 CL 调用全用反射,不假设类型可达 +- `IncisionGateHost` 放在系统 CL,成为所有插件共享的单点 + +这套机制能跑但不优雅——**根本上说,Minecraft 插件生态的 CL 设计不是为 AOP 友好的**。 + +### 10.4 诊断难度 + +字节码层 bug 的错误信息通常是 `VerifyError` 或 `ClassFormatError`,对用户不友好。Incision 的对策: + +- `Forensics` 模块把 warn/error 结构化 +- `Trauma` 异常类型把失败分类 +- `FrameVerifier` 提前拦截坏字节码 +- `incision.dev=true` 时把织入前后的 class 文件 dump 到磁盘,便于人工比对 + +--- + +## 11. FAQ + +### Q: Incision 会修改原本类的类名吗? + +不会。类名、Class 对象、父类、接口、字段表、方法表(签名)全部保留。只有方法体内部的字节码被修改。 + +### Q: 是动态代理吗? + +不是。动态代理会生成新类,而且需要接口、只能拦通过代理对象的调用。Incision 不创建新类,拦的是原类本身,自调用 / final / NMS 内部调用都能拦。 + +### Q: 那是怎么做到"让原类变成新行为"的? + +JVM 的 `Instrumentation.retransformClasses` / JVMTI `RetransformClasses` 允许**原地替换已加载类的方法字节码**。Incision 把 ASM 生成的新字节码交给 JVM,JVM 内部把对应的方法体换掉。Class 对象身份不变,旧实例继续有效,下次调用走新字节码。 + +### Q: 为什么需要 IncisionBridge 这个 Java 类? + +因为 TabooLib Gradle 插件会重定位 `taboolib.*` 包名。如果织入字节码里写死 `taboolib.module.incision.runtime.TheatreDispatcher`,每个插件打包后这个类的全限定名都不同,无法共享同一个 dispatcher。桥放在不被重定位的 `io.izzel.*` 包,所有插件指向同一个桥。 + +### Q: JvmtiBackend 和 InstrumentationBackend 有什么区别? + +两者最终都调 JVM 同一个底层 API(RetransformClasses),但拿到这个 API 的**路径**不同: + +- Instrumentation:通过 `java.lang.instrument` → self-attach → Agent。受 JVM 的 attach 权限管制。 +- JVMTI:直接 `System.load()` 装载自带的 native agent,绕过 attach 机制。 + +此外 JVMTI 后端还多了原字节码缓存、任意 CL 的 `defineClass`、无访问控制的字段/方法访问等独家能力。 + +### Q: 那为什么不直接只用 JvmtiBackend? + +需要预编译 6 个平台二进制,jar 体积会变大(native 库总共几百 KB)。Instrumentation 纯 Java 实现,更轻量。在开发机和允许 self-attach 的环境下 Instrumentation 够用。JvmtiBackend 是生产环境兜底和能力扩展。 + +### Q: 能在 `@Awake(ENABLE)` 里声明 advice 吗? + +能,但效果等同 ENABLE 之后才织入——ENABLE 之前已经执行过的代码不会被拦截。推荐改成 `@Surgeon` 注解模式,扫描发生在 CONST 阶段,能覆盖 INIT/LOAD/ENABLE 阶段的宿主代码。 + +### Q: 会和其他字节码框架(Mixin / AspectJ)冲突吗? + +不会必然冲突。所有字节码框架都基于 ASM 和 JVM retransform,机制兼容。但如果两个框架织同一个方法的同一个位点,字节码层的叠加顺序取决于 transformer 注册顺序,可能产生意外结果。建议隔离职责域。 + +### Q: 运行时能"卸载"某个 advice 吗? + +能,通过 `Suture.heal()` 或 `close()`。但"卸载"不是回滚字节码(字节码里的 dispatch 调用还在),而是让 dispatcher 在那个 target 上不再有 handler 可执行——调用进来 bridge 后直接返回 null,控制权回到原方法体。 + +### Q: 为什么 `suspend` 不直接回滚字节码? + +回滚字节码意味着再做一次 retransform,有成本而且非原子。dispatcher 层的 suspend 只是改一个标志位,纳秒级开销。对"临时停掉"这种高频操作,dispatcher 旁路比字节码回滚合算得多。 + +--- + +## 附:关键文件索引 + +| 路径 | 作用 | +|---|---| +| `weaver/Scalpel.kt` | 主 weaver,对单个类做注入 | +| `weaver/SiteWeaver.kt` | Tree API 路径的方法级织入驱动 | +| `weaver/FrameVerifier.kt` | 字节码 verify 预检安全网 | +| `loader/Backend.kt` | 后端接口 | +| `loader/InstrumentationBackend.kt` | self-attach 路径 | +| `loader/JvmtiBackend.kt` | native JVMTI 路径 | +| `loader/ClassLoaderHookBackend.kt` | 占位,委派给 Instrumentation | +| `bridge/IncisionBridge.java` | 不被重定位的 Java 桥 | +| `runtime/TheatreDispatcher.kt` | advice 链调度中心 | +| `pred/PredCompiler.kt` | `where` DSL 的独立字节码编译器 | +| `api/IncisionAccessor.kt` | 字段/方法访问底层门面(解析缓存 + JVMTI/反射/Unsafe 三级 fallback) | +| `api/Accessors.kt` | Lambda 工厂 + Accessor 类族(handler 字段/方法访问的主推 API) | +| `src/main/c/incision_jvmti.c` | JVMTI native agent 源码 | diff --git a/module/incision/build.gradle.kts b/module/incision/build.gradle.kts new file mode 100644 index 000000000..b12c474f8 --- /dev/null +++ b/module/incision/build.gradle.kts @@ -0,0 +1,17 @@ +dependencies { + compileOnly(project(":common")) + compileOnly(project(":common-env")) + compileOnly(project(":common-platform-api")) + compileOnly(project(":common-util")) + compileOnly(project(":common-platform-api")) + compileOnly(project(":platform:platform-bukkit")) + compileOnly(project(":platform:platform-bukkit-impl")) + // 可选 — 检测到时启用 NMS NameResolver + compileOnly(project(":module:bukkit-nms")) + // ASM + compileOnly("org.ow2.asm:asm:9.8") + compileOnly("org.ow2.asm:asm-commons:9.8") + compileOnly("org.ow2.asm:asm-util:9.8") + // self-attach 默认路径 + compileOnly("net.bytebuddy:byte-buddy-agent:1.14.18") +} diff --git a/module/incision/src/main/c/build-all.bat b/module/incision/src/main/c/build-all.bat new file mode 100644 index 000000000..d9e20c1f0 --- /dev/null +++ b/module/incision/src/main/c/build-all.bat @@ -0,0 +1,51 @@ +@echo off +setlocal + +set "CDIR=%~dp0" +set "SRC=%CDIR%incision_jvmti.c" +set "RES=%CDIR%..\resources\native" + +:: JNI headers - use JAVA_HOME if set, otherwise try D:\Java\jdk-21 +if defined JAVA_HOME ( + set "JNI=%JAVA_HOME%\include" +) else ( + set "JNI=D:\Java\jdk-21\include" +) + +if not exist "%JNI%\jni.h" ( + echo ERROR: jni.h not found at %JNI% + echo Set JAVA_HOME or edit this script + exit /b 1 +) + +echo === Building Incision JVMTI native for all platforms === +echo Using JNI headers: %JNI% +echo. + +echo [1/6] Windows x64 ... +zig cc -shared -O2 -I"%JNI%" -I"%JNI%\win32" -target x86_64-windows-gnu "%SRC%" -o "%RES%\windows\x64\incision-jvmti.dll" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo [2/6] Windows arm64 ... +zig cc -shared -O2 -I"%JNI%" -I"%JNI%\win32" -target aarch64-windows-gnu "%SRC%" -o "%RES%\windows\arm64\incision-jvmti.dll" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo [3/6] Linux x64 ... +zig cc -shared -fPIC -O2 -I"%JNI%" -I"%CDIR%include\linux" -target x86_64-linux-gnu "%SRC%" -o "%RES%\linux\x64\libincision-jvmti.so" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo [4/6] Linux arm64 ... +zig cc -shared -fPIC -O2 -I"%JNI%" -I"%CDIR%include\linux" -target aarch64-linux-gnu "%SRC%" -o "%RES%\linux\arm64\libincision-jvmti.so" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo [5/6] macOS x64 ... +zig cc -shared -fPIC -O2 -I"%JNI%" -I"%CDIR%include\darwin" -target x86_64-macos-none "%SRC%" -o "%RES%\macos\x64\libincision-jvmti.dylib" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo [6/6] macOS arm64 ... +zig cc -shared -fPIC -O2 -I"%JNI%" -I"%CDIR%include\darwin" -target aarch64-macos-none "%SRC%" -o "%RES%\macos\arm64\libincision-jvmti.dylib" +if %errorlevel% neq 0 ( echo FAILED ) else ( echo OK ) + +echo. +echo === Done === +endlocal diff --git a/module/incision/src/main/c/incision_jvmti.c b/module/incision/src/main/c/incision_jvmti.c new file mode 100644 index 000000000..0dcd9112a --- /dev/null +++ b/module/incision/src/main/c/incision_jvmti.c @@ -0,0 +1,1162 @@ +/* + * incision_jvmti.c — Minimal JVMTI native library for Incision. + * + * Provides retransformClasses + ClassFileLoadHook without requiring + * -javaagent or -XX:+EnableDynamicAgentLoading. + * + * Uses JNI dynamic registration via JNI_OnLoad so that the Java-side + * class name can be freely relocated by Gradle Shadow without breaking + * the native linkage. The Kotlin side sets system property + * "incision.jvmti.class" to the (possibly relocated) class name before + * calling System.load(). + * + * Build (Windows MSVC): + * cl /LD /O2 /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" + * incision_jvmti.c /Fe:incision-jvmti.dll + * + * Build (Linux GCC): + * gcc -shared -fPIC -O2 -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" + * incision_jvmti.c -o libincision-jvmti.so + * + * Build (macOS): + * gcc -shared -fPIC -O2 -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" + * incision_jvmti.c -o libincision-jvmti.dylib + * + * Apache-2.0 + */ + +#include +#include +#include +#include + +/* ================================================================== */ +/* Global state */ +/* ================================================================== */ + +static jvmtiEnv *g_jvmti = NULL; +static JavaVM *g_jvm = NULL; +static jclass g_backend_class = NULL; +static jmethodID g_callback_mid = NULL; + +/* ================================================================== */ +/* 字节码缓存(open-addressing hashmap,线性探测) */ +/* ================================================================== */ + +#define CACHE_CAPACITY 512 + +typedef struct { + char *key; /* owner internal name(strdup 分配,NULL 表示空槽) */ + jbyte *bytes; /* 原始字节码(malloc 分配) */ + jint len; /* 字节码长度 */ +} CacheEntry; + +static CacheEntry g_cache[CACHE_CAPACITY]; +static int g_cache_count = 0; + +/* JVMTI raw monitor,用于保护缓存并发访问 */ +static jrawMonitorID g_monitor = NULL; + +/* 二次 retransform 协调:dummy extract 模式 */ +static volatile int g_extract_mode = 0; +static jbyte *g_extract_buf = NULL; +static jint g_extract_len = 0; + +/* djb2 字符串哈希 */ +static unsigned int hash_str(const char *s) { + unsigned int h = 5381; + int c; + while ((c = (unsigned char)*s++) != 0) { + h = ((h << 5) + h) + c; /* h * 33 + c */ + } + return h; +} + +/* 查找 key 对应的槽索引,找不到返回 -1 */ +static int cache_find(const char *key) { + if (key == NULL) return -1; + unsigned int h = hash_str(key) % CACHE_CAPACITY; + for (int i = 0; i < CACHE_CAPACITY; i++) { + int idx = (int)((h + i) % CACHE_CAPACITY); + if (g_cache[idx].key == NULL) return -1; /* 空槽,终止探测 */ + if (strcmp(g_cache[idx].key, key) == 0) return idx; + } + return -1; +} + +/* 存入或覆盖缓存条目;成功返回 1,哈希表满返回 0 */ +static int cache_put(const char *key, const jbyte *bytes, jint len) { + if (key == NULL) return 0; + unsigned int h = hash_str(key) % CACHE_CAPACITY; + for (int i = 0; i < CACHE_CAPACITY; i++) { + int idx = (int)((h + i) % CACHE_CAPACITY); + if (g_cache[idx].key == NULL) { + /* 新条目:占用空槽 */ + g_cache[idx].key = strdup(key); + if (g_cache[idx].key == NULL) return 0; + g_cache[idx].bytes = (jbyte *)bytes; + g_cache[idx].len = len; + g_cache_count++; + return 1; + } + if (strcmp(g_cache[idx].key, key) == 0) { + /* 覆盖现有条目:释放旧字节码 */ + if (g_cache[idx].bytes != NULL) free(g_cache[idx].bytes); + g_cache[idx].bytes = (jbyte *)bytes; + g_cache[idx].len = len; + return 1; + } + } + return 0; /* 哈希表已满 */ +} + +/* 移除并释放 key 对应的条目 */ +static void cache_remove(const char *key) { + int idx = cache_find(key); + if (idx < 0) return; + if (g_cache[idx].key != NULL) { free(g_cache[idx].key); g_cache[idx].key = NULL; } + if (g_cache[idx].bytes != NULL) { free(g_cache[idx].bytes); g_cache[idx].bytes = NULL; } + g_cache[idx].len = 0; + g_cache_count--; + /* 注:不做线性探测 rehash,因 cache_find 遇 NULL 即停; + 被移除槽后续的同哈希链条目再次查询时会返回 -1。 + 可接受代价:同链条目在删除后变得不可达, + 但 Incision 用法以写入和读取为主,很少删除,因此暂不实现 rehash。 */ +} + +/* 清空全部缓存 */ +static void cache_clear() { + for (int i = 0; i < CACHE_CAPACITY; i++) { + if (g_cache[i].key != NULL) { free(g_cache[i].key); g_cache[i].key = NULL; } + if (g_cache[i].bytes != NULL) { free(g_cache[i].bytes); g_cache[i].bytes = NULL; } + g_cache[i].len = 0; + } + g_cache_count = 0; +} + +/* ================================================================== */ +/* ClassFileLoadHook */ +/* ================================================================== */ + +static void JNICALL classFileLoadHook( + jvmtiEnv *jvmti, JNIEnv *jni, + jclass class_being_redefined, + jobject loader, + const char *name, + jobject protection_domain, + jint class_data_len, + const unsigned char *class_data, + jint *new_class_data_len, + unsigned char **new_class_data) { + + /* 抽取模式:复制原始字节码到临时缓冲区,不修改类 */ + if (g_extract_mode) { + g_extract_buf = (jbyte *)malloc(class_data_len); + if (g_extract_buf != NULL) { + memcpy(g_extract_buf, class_data, class_data_len); + g_extract_len = class_data_len; + } + return; /* 不设置 new_class_data,类保持原样 */ + } + + if (g_callback_mid == NULL || name == NULL) return; + + jstring jname = (*jni)->NewStringUTF(jni, name); + if (jname == NULL) return; + + jbyteArray jbytes = (*jni)->NewByteArray(jni, class_data_len); + if (jbytes == NULL) { (*jni)->DeleteLocalRef(jni, jname); return; } + (*jni)->SetByteArrayRegion(jni, jbytes, 0, class_data_len, (const jbyte *)class_data); + + jbyteArray result = (jbyteArray)(*jni)->CallStaticObjectMethod( + jni, g_backend_class, g_callback_mid, loader, jname, jbytes); + + if ((*jni)->ExceptionCheck(jni)) { + (*jni)->ExceptionClear(jni); + (*jni)->DeleteLocalRef(jni, jname); + (*jni)->DeleteLocalRef(jni, jbytes); + return; + } + + if (result != NULL) { + jint len = (*jni)->GetArrayLength(jni, result); + unsigned char *buf = NULL; + (*jvmti)->Allocate(jvmti, len, &buf); + if (buf != NULL) { + (*jni)->GetByteArrayRegion(jni, result, 0, len, (jbyte *)buf); + *new_class_data = buf; + *new_class_data_len = len; + } + (*jni)->DeleteLocalRef(jni, result); + } + + (*jni)->DeleteLocalRef(jni, jname); + (*jni)->DeleteLocalRef(jni, jbytes); +} + +/* ================================================================== */ +/* Obtain jvmtiEnv */ +/* ================================================================== */ + +static int ensureJvmti(JNIEnv *jni) { + if (g_jvmti != NULL) return 1; + + if (g_jvm == NULL) { + if ((*jni)->GetJavaVM(jni, &g_jvm) != JNI_OK || g_jvm == NULL) return 0; + } + + if ((*g_jvm)->GetEnv(g_jvm, (void **)&g_jvmti, JVMTI_VERSION_1_2) != JNI_OK + || g_jvmti == NULL) { + return 0; + } + + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_retransform_classes = 1; + caps.can_generate_all_class_hook_events = 1; + if ((*g_jvmti)->AddCapabilities(g_jvmti, &caps) != JVMTI_ERROR_NONE) { + g_jvmti = NULL; + return 0; + } + + jvmtiEventCallbacks cb; + memset(&cb, 0, sizeof(cb)); + cb.ClassFileLoadHook = &classFileLoadHook; + (*g_jvmti)->SetEventCallbacks(g_jvmti, &cb, sizeof(cb)); + (*g_jvmti)->SetEventNotificationMode( + g_jvmti, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); + + /* 创建 raw monitor 用于缓存及 extract 模式的互斥保护 */ + if (g_monitor == NULL) { + (*g_jvmti)->CreateRawMonitor(g_jvmti, "incision-cache", &g_monitor); + } + + return 1; +} + +/* ================================================================== */ +/* Native method implementations (registered dynamically) */ +/* ================================================================== */ + +static jboolean JNICALL nInit(JNIEnv *jni, jclass self, jclass backendClass) { + if (!ensureJvmti(jni)) return JNI_FALSE; + + g_backend_class = (jclass)(*jni)->NewGlobalRef(jni, backendClass); + if (g_backend_class == NULL) return JNI_FALSE; + + g_callback_mid = (*jni)->GetStaticMethodID(jni, g_backend_class, + "onClassFileLoad", + "(Ljava/lang/ClassLoader;Ljava/lang/String;[B)[B"); + if (g_callback_mid == NULL) { + (*jni)->DeleteGlobalRef(jni, g_backend_class); + g_backend_class = NULL; + return JNI_FALSE; + } + return JNI_TRUE; +} + +static jboolean JNICALL nRetransform(JNIEnv *jni, jclass self, jclass target) { + if (g_jvmti == NULL) return JNI_FALSE; + jvmtiError err = (*g_jvmti)->RetransformClasses(g_jvmti, 1, &target); + if (err != JVMTI_ERROR_NONE) { + /* 输出 JVMTI 错误码,便于诊断静默失败 */ + fprintf(stderr, "[Incision][JVMTI] RetransformClasses 失败: error=%d\n", (int)err); + fflush(stderr); + } + return err == JVMTI_ERROR_NONE ? JNI_TRUE : JNI_FALSE; +} + +static jboolean JNICALL nAvailable(JNIEnv *jni, jclass self) { + return g_jvmti != NULL ? JNI_TRUE : JNI_FALSE; +} + +static jclass JNICALL nDefineClass(JNIEnv *jni, jclass self, jobject loader, jstring name, jbyteArray bytes) { + const char *nameStr = (*jni)->GetStringUTFChars(jni, name, NULL); + if (nameStr == NULL) return NULL; + + jsize len = (*jni)->GetArrayLength(jni, bytes); + jbyte *buf = (*jni)->GetByteArrayElements(jni, bytes, NULL); + if (buf == NULL) { + (*jni)->ReleaseStringUTFChars(jni, name, nameStr); + return NULL; + } + + jclass result = (*jni)->DefineClass(jni, nameStr, loader, buf, len); + + (*jni)->ReleaseByteArrayElements(jni, bytes, buf, JNI_ABORT); + (*jni)->ReleaseStringUTFChars(jni, name, nameStr); + + if ((*jni)->ExceptionCheck(jni)) { + (*jni)->ExceptionClear(jni); + return NULL; + } + + return result; +} + +static void JNICALL nDispose(JNIEnv *jni, jclass self) { + if (g_jvmti != NULL) { + (*g_jvmti)->SetEventNotificationMode( + g_jvmti, JVMTI_DISABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); + } + if (g_backend_class != NULL) { + (*jni)->DeleteGlobalRef(jni, g_backend_class); + g_backend_class = NULL; + } + g_callback_mid = NULL; + + /* 释放所有缓存字节码 */ + if (g_jvmti != NULL && g_monitor != NULL) { + (*g_jvmti)->RawMonitorEnter(g_jvmti, g_monitor); + cache_clear(); + (*g_jvmti)->RawMonitorExit(g_jvmti, g_monitor); + } else { + cache_clear(); + } +} + +/* ================================================================== */ +/* 字节码缓存相关 native 方法 */ +/* ================================================================== */ + +/* 缓存指定 owner 的原始字节码 */ +static jboolean JNICALL native_cache_original(JNIEnv *env, jclass clz, jstring name, jbyteArray bytes) { + if (g_jvmti == NULL || g_monitor == NULL) return JNI_FALSE; + const char *cname = (*env)->GetStringUTFChars(env, name, NULL); + if (cname == NULL) return JNI_FALSE; + + jint len = (*env)->GetArrayLength(env, bytes); + jbyte *buf = (jbyte *)malloc(len); + if (buf == NULL) { + (*env)->ReleaseStringUTFChars(env, name, cname); + return JNI_FALSE; + } + (*env)->GetByteArrayRegion(env, bytes, 0, len, buf); + + (*g_jvmti)->RawMonitorEnter(g_jvmti, g_monitor); + int ok = cache_put(cname, buf, len); + (*g_jvmti)->RawMonitorExit(g_jvmti, g_monitor); + + (*env)->ReleaseStringUTFChars(env, name, cname); + if (!ok) free(buf); /* cache_put 失败(哈希表满),释放刚分配的缓冲区 */ + return ok ? JNI_TRUE : JNI_FALSE; +} + +/* 读取指定 owner 的缓存字节码,未命中返回 null */ +static jbyteArray JNICALL native_get_cached(JNIEnv *env, jclass clz, jstring name) { + if (g_jvmti == NULL || g_monitor == NULL) return NULL; + const char *cname = (*env)->GetStringUTFChars(env, name, NULL); + if (cname == NULL) return NULL; + + (*g_jvmti)->RawMonitorEnter(g_jvmti, g_monitor); + int idx = cache_find(cname); + jbyteArray result = NULL; + if (idx >= 0) { + result = (*env)->NewByteArray(env, g_cache[idx].len); + if (result != NULL) { + (*env)->SetByteArrayRegion(env, result, 0, g_cache[idx].len, g_cache[idx].bytes); + } + } + (*g_jvmti)->RawMonitorExit(g_jvmti, g_monitor); + + (*env)->ReleaseStringUTFChars(env, name, cname); + return result; +} + +/* 通过 dummy retransform 抽取目标类当前的字节码 */ +static jbyteArray JNICALL native_extract_bytes(JNIEnv *env, jclass clz, jobject target) { + if (g_jvmti == NULL || g_monitor == NULL) return NULL; + + jclass targetClass = (jclass)target; + + (*g_jvmti)->RawMonitorEnter(g_jvmti, g_monitor); + /* 进入抽取模式;ClassFileLoadHook 将仅复制 bytes,不触发 Kotlin 回调 */ + g_extract_mode = 1; + g_extract_buf = NULL; + g_extract_len = 0; + + jvmtiError err = (*g_jvmti)->RetransformClasses(g_jvmti, 1, &targetClass); + + g_extract_mode = 0; + + jbyteArray result = NULL; + if (err == JVMTI_ERROR_NONE && g_extract_buf != NULL) { + result = (*env)->NewByteArray(env, g_extract_len); + if (result != NULL) { + (*env)->SetByteArrayRegion(env, result, 0, g_extract_len, g_extract_buf); + } + } + + if (g_extract_buf != NULL) { free(g_extract_buf); g_extract_buf = NULL; } + g_extract_len = 0; + (*g_jvmti)->RawMonitorExit(g_jvmti, g_monitor); + + return result; +} + +/* 从缓存中移除指定 owner 的条目 */ +static void JNICALL native_purge_cache(JNIEnv *env, jclass clz, jstring name) { + if (g_jvmti == NULL || g_monitor == NULL) return; + const char *cname = (*env)->GetStringUTFChars(env, name, NULL); + if (cname == NULL) return; + (*g_jvmti)->RawMonitorEnter(g_jvmti, g_monitor); + cache_remove(cname); + (*g_jvmti)->RawMonitorExit(g_jvmti, g_monitor); + (*env)->ReleaseStringUTFChars(env, name, cname); +} + +/* ================================================================== */ +/* 通用字段/方法访问器 — 绕过 Java 访问控制 */ +/* */ +/* JNI 的 GetFieldID / GetMethodID / Get*Field / Set*Field / */ +/* Call*Method 不受 private / protected / 模块边界限制。 */ +/* 此组 API 为 BodiesClassGenerator 和其他运行时组件提供通用底层访问。 */ +/* ================================================================== */ + +/** + * 根据 fieldDesc 的首字符判断基本类型,装箱后返回 jobject。 + * 非基本类型直接返回 GetObjectField 结果。 + */ +static jobject JNICALL native_field_get(JNIEnv *env, jclass clz, + jobject obj, jclass ownerClass, jstring fieldName, jstring fieldDesc) { + const char *fname = (*env)->GetStringUTFChars(env, fieldName, NULL); + const char *fdesc = (*env)->GetStringUTFChars(env, fieldDesc, NULL); + if (fname == NULL || fdesc == NULL) { + if (fname) (*env)->ReleaseStringUTFChars(env, fieldName, fname); + if (fdesc) (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return NULL; + } + + jfieldID fid = (*env)->GetFieldID(env, ownerClass, fname, fdesc); + if (fid == NULL) { + (*env)->ExceptionClear(env); + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return NULL; + } + + jobject result = NULL; + char type = fdesc[0]; + + /* 根据描述符首字符分发到对应的 JNI Get*Field 并装箱 */ + switch (type) { + case 'Z': { /* boolean */ + jboolean v = (*env)->GetBooleanField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(Z)Ljava/lang/Boolean;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'B': { /* byte */ + jbyte v = (*env)->GetByteField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(B)Ljava/lang/Byte;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'C': { /* char */ + jchar v = (*env)->GetCharField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(C)Ljava/lang/Character;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'S': { /* short */ + jshort v = (*env)->GetShortField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(S)Ljava/lang/Short;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'I': { /* int */ + jint v = (*env)->GetIntField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'J': { /* long */ + jlong v = (*env)->GetLongField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'F': { /* float */ + jfloat v = (*env)->GetFloatField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(F)Ljava/lang/Float;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'D': { /* double */ + jdouble v = (*env)->GetDoubleField(env, obj, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(D)Ljava/lang/Double;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + default: { /* 对象或数组类型(L... 或 [... ) */ + result = (*env)->GetObjectField(env, obj, fid); + break; + } + } + + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return result; +} + +/** + * 设置实例字段值。value 为装箱后的 Object,根据 fieldDesc 拆箱写入。 + */ +static void JNICALL native_field_set(JNIEnv *env, jclass clz, + jobject obj, jclass ownerClass, jstring fieldName, jstring fieldDesc, jobject value) { + const char *fname = (*env)->GetStringUTFChars(env, fieldName, NULL); + const char *fdesc = (*env)->GetStringUTFChars(env, fieldDesc, NULL); + if (fname == NULL || fdesc == NULL) { + if (fname) (*env)->ReleaseStringUTFChars(env, fieldName, fname); + if (fdesc) (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return; + } + + jfieldID fid = (*env)->GetFieldID(env, ownerClass, fname, fdesc); + if (fid == NULL) { + (*env)->ExceptionClear(env); + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return; + } + + char type = fdesc[0]; + + /* 根据描述符首字符拆箱并调用对应的 Set*Field */ + switch (type) { + case 'Z': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "booleanValue", "()Z"); + jboolean v = (*env)->CallBooleanMethod(env, value, unbox); + (*env)->SetBooleanField(env, obj, fid, v); + break; + } + case 'B': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "byteValue", "()B"); + jbyte v = (*env)->CallByteMethod(env, value, unbox); + (*env)->SetByteField(env, obj, fid, v); + break; + } + case 'C': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "charValue", "()C"); + jchar v = (*env)->CallCharMethod(env, value, unbox); + (*env)->SetCharField(env, obj, fid, v); + break; + } + case 'S': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "shortValue", "()S"); + jshort v = (*env)->CallShortMethod(env, value, unbox); + (*env)->SetShortField(env, obj, fid, v); + break; + } + case 'I': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "intValue", "()I"); + jint v = (*env)->CallIntMethod(env, value, unbox); + (*env)->SetIntField(env, obj, fid, v); + break; + } + case 'J': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "longValue", "()J"); + jlong v = (*env)->CallLongMethod(env, value, unbox); + (*env)->SetLongField(env, obj, fid, v); + break; + } + case 'F': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "floatValue", "()F"); + jfloat v = (*env)->CallFloatMethod(env, value, unbox); + (*env)->SetFloatField(env, obj, fid, v); + break; + } + case 'D': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "doubleValue", "()D"); + jdouble v = (*env)->CallDoubleMethod(env, value, unbox); + (*env)->SetDoubleField(env, obj, fid, v); + break; + } + default: { + (*env)->SetObjectField(env, obj, fid, value); + break; + } + } + + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); +} + +/** + * 读取静态字段值并装箱返回。 + */ +static jobject JNICALL native_static_field_get(JNIEnv *env, jclass clz, + jclass ownerClass, jstring fieldName, jstring fieldDesc) { + const char *fname = (*env)->GetStringUTFChars(env, fieldName, NULL); + const char *fdesc = (*env)->GetStringUTFChars(env, fieldDesc, NULL); + if (fname == NULL || fdesc == NULL) { + if (fname) (*env)->ReleaseStringUTFChars(env, fieldName, fname); + if (fdesc) (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return NULL; + } + + jfieldID fid = (*env)->GetStaticFieldID(env, ownerClass, fname, fdesc); + if (fid == NULL) { + (*env)->ExceptionClear(env); + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return NULL; + } + + jobject result = NULL; + char type = fdesc[0]; + + switch (type) { + case 'Z': { + jboolean v = (*env)->GetStaticBooleanField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(Z)Ljava/lang/Boolean;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'B': { + jbyte v = (*env)->GetStaticByteField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(B)Ljava/lang/Byte;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'C': { + jchar v = (*env)->GetStaticCharField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(C)Ljava/lang/Character;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'S': { + jshort v = (*env)->GetStaticShortField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(S)Ljava/lang/Short;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'I': { + jint v = (*env)->GetStaticIntField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'J': { + jlong v = (*env)->GetStaticLongField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'F': { + jfloat v = (*env)->GetStaticFloatField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(F)Ljava/lang/Float;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'D': { + jdouble v = (*env)->GetStaticDoubleField(env, ownerClass, fid); + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(D)Ljava/lang/Double;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + default: { + result = (*env)->GetStaticObjectField(env, ownerClass, fid); + break; + } + } + + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return result; +} + +/** + * 设置静态字段值。value 为装箱 Object,根据 fieldDesc 拆箱写入。 + */ +static void JNICALL native_static_field_set(JNIEnv *env, jclass clz, + jclass ownerClass, jstring fieldName, jstring fieldDesc, jobject value) { + const char *fname = (*env)->GetStringUTFChars(env, fieldName, NULL); + const char *fdesc = (*env)->GetStringUTFChars(env, fieldDesc, NULL); + if (fname == NULL || fdesc == NULL) { + if (fname) (*env)->ReleaseStringUTFChars(env, fieldName, fname); + if (fdesc) (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return; + } + + jfieldID fid = (*env)->GetStaticFieldID(env, ownerClass, fname, fdesc); + if (fid == NULL) { + (*env)->ExceptionClear(env); + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); + return; + } + + char type = fdesc[0]; + + switch (type) { + case 'Z': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "booleanValue", "()Z"); + jboolean v = (*env)->CallBooleanMethod(env, value, unbox); + (*env)->SetStaticBooleanField(env, ownerClass, fid, v); + break; + } + case 'B': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "byteValue", "()B"); + jbyte v = (*env)->CallByteMethod(env, value, unbox); + (*env)->SetStaticByteField(env, ownerClass, fid, v); + break; + } + case 'C': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "charValue", "()C"); + jchar v = (*env)->CallCharMethod(env, value, unbox); + (*env)->SetStaticCharField(env, ownerClass, fid, v); + break; + } + case 'S': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "shortValue", "()S"); + jshort v = (*env)->CallShortMethod(env, value, unbox); + (*env)->SetStaticShortField(env, ownerClass, fid, v); + break; + } + case 'I': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "intValue", "()I"); + jint v = (*env)->CallIntMethod(env, value, unbox); + (*env)->SetStaticIntField(env, ownerClass, fid, v); + break; + } + case 'J': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "longValue", "()J"); + jlong v = (*env)->CallLongMethod(env, value, unbox); + (*env)->SetStaticLongField(env, ownerClass, fid, v); + break; + } + case 'F': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "floatValue", "()F"); + jfloat v = (*env)->CallFloatMethod(env, value, unbox); + (*env)->SetStaticFloatField(env, ownerClass, fid, v); + break; + } + case 'D': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "doubleValue", "()D"); + jdouble v = (*env)->CallDoubleMethod(env, value, unbox); + (*env)->SetStaticDoubleField(env, ownerClass, fid, v); + break; + } + default: { + (*env)->SetStaticObjectField(env, ownerClass, fid, value); + break; + } + } + + (*env)->ReleaseStringUTFChars(env, fieldName, fname); + (*env)->ReleaseStringUTFChars(env, fieldDesc, fdesc); +} + +/** + * 通用方法调用器 — 绕过 Java 访问控制调用任意实例/静态方法。 + * + * 签名:nInvokeMethod(Object obj, Class ownerClass, String methodName, + * String methodDesc, Object[] args) → Object + * + * - obj 为 null 时按静态方法调用 + * - 返回值自动装箱;void 方法返回 null + * - args 中基本类型已装箱,由本函数拆箱后传入 JNI Call*Method + * + * 当前实现使用 jvalue 数组 + CallObjectMethodA 简化变长参数处理。 + * 基本类型返回值通过对应的 JNI Call*Method 获取后装箱。 + */ +static jobject JNICALL native_invoke_method(JNIEnv *env, jclass clz, + jobject obj, jclass ownerClass, jstring methodName, jstring methodDesc, jobjectArray args) { + const char *mname = (*env)->GetStringUTFChars(env, methodName, NULL); + const char *mdesc = (*env)->GetStringUTFChars(env, methodDesc, NULL); + if (mname == NULL || mdesc == NULL) { + if (mname) (*env)->ReleaseStringUTFChars(env, methodName, mname); + if (mdesc) (*env)->ReleaseStringUTFChars(env, methodDesc, mdesc); + return NULL; + } + + int isStatic = (obj == NULL); + jmethodID mid; + if (isStatic) { + mid = (*env)->GetStaticMethodID(env, ownerClass, mname, mdesc); + } else { + mid = (*env)->GetMethodID(env, ownerClass, mname, mdesc); + } + if (mid == NULL) { + (*env)->ExceptionClear(env); + (*env)->ReleaseStringUTFChars(env, methodName, mname); + (*env)->ReleaseStringUTFChars(env, methodDesc, mdesc); + return NULL; + } + + /* 解析参数描述符,构造 jvalue 数组 */ + int argCount = args != NULL ? (*env)->GetArrayLength(env, args) : 0; + jvalue *jargs = NULL; + if (argCount > 0) { + jargs = (jvalue *)calloc(argCount, sizeof(jvalue)); + if (jargs == NULL) { + (*env)->ReleaseStringUTFChars(env, methodName, mname); + (*env)->ReleaseStringUTFChars(env, methodDesc, mdesc); + return NULL; + } + + /* 遍历描述符中的参数类型,逐个拆箱 */ + const char *p = mdesc + 1; /* 跳过 '(' */ + for (int i = 0; i < argCount && *p != ')'; i++) { + jobject arg = (*env)->GetObjectArrayElement(env, args, i); + switch (*p) { + case 'Z': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "booleanValue", "()Z"); + jargs[i].z = (*env)->CallBooleanMethod(env, arg, unbox); + p++; + break; + } + case 'B': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "byteValue", "()B"); + jargs[i].b = (*env)->CallByteMethod(env, arg, unbox); + p++; + break; + } + case 'C': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "charValue", "()C"); + jargs[i].c = (*env)->CallCharMethod(env, arg, unbox); + p++; + break; + } + case 'S': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "shortValue", "()S"); + jargs[i].s = (*env)->CallShortMethod(env, arg, unbox); + p++; + break; + } + case 'I': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "intValue", "()I"); + jargs[i].i = (*env)->CallIntMethod(env, arg, unbox); + p++; + break; + } + case 'J': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "longValue", "()J"); + jargs[i].j = (*env)->CallLongMethod(env, arg, unbox); + p++; + break; + } + case 'F': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "floatValue", "()F"); + jargs[i].f = (*env)->CallFloatMethod(env, arg, unbox); + p++; + break; + } + case 'D': { + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID unbox = (*env)->GetMethodID(env, boxCls, "doubleValue", "()D"); + jargs[i].d = (*env)->CallDoubleMethod(env, arg, unbox); + p++; + break; + } + case 'L': { + jargs[i].l = arg; + /* 跳过 'L...;' */ + while (*p && *p != ';') p++; + if (*p == ';') p++; + break; + } + case '[': { + jargs[i].l = arg; + /* 跳过数组维度前缀 */ + while (*p == '[') p++; + if (*p == 'L') { + while (*p && *p != ';') p++; + if (*p == ';') p++; + } else { + p++; /* 基本类型数组 */ + } + break; + } + default: + jargs[i].l = arg; + p++; + break; + } + } + } + + /* 解析返回类型 */ + const char *retStart = strchr(mdesc, ')'); + char retType = retStart ? *(retStart + 1) : 'V'; + + jobject result = NULL; + + /* 根据返回类型分发调用 */ + if (isStatic) { + switch (retType) { + case 'V': + (*env)->CallStaticVoidMethodA(env, ownerClass, mid, jargs); + break; + case 'Z': { + jboolean v = (*env)->CallStaticBooleanMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(Z)Ljava/lang/Boolean;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'B': { + jbyte v = (*env)->CallStaticByteMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(B)Ljava/lang/Byte;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'C': { + jchar v = (*env)->CallStaticCharMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(C)Ljava/lang/Character;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'S': { + jshort v = (*env)->CallStaticShortMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(S)Ljava/lang/Short;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'I': { + jint v = (*env)->CallStaticIntMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'J': { + jlong v = (*env)->CallStaticLongMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'F': { + jfloat v = (*env)->CallStaticFloatMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(F)Ljava/lang/Float;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'D': { + jdouble v = (*env)->CallStaticDoubleMethodA(env, ownerClass, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(D)Ljava/lang/Double;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + default: /* 引用类型 */ + result = (*env)->CallStaticObjectMethodA(env, ownerClass, mid, jargs); + break; + } + } else { + switch (retType) { + case 'V': + (*env)->CallVoidMethodA(env, obj, mid, jargs); + break; + case 'Z': { + jboolean v = (*env)->CallBooleanMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Boolean"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(Z)Ljava/lang/Boolean;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'B': { + jbyte v = (*env)->CallByteMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Byte"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(B)Ljava/lang/Byte;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'C': { + jchar v = (*env)->CallCharMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Character"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(C)Ljava/lang/Character;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'S': { + jshort v = (*env)->CallShortMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Short"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(S)Ljava/lang/Short;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'I': { + jint v = (*env)->CallIntMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'J': { + jlong v = (*env)->CallLongMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'F': { + jfloat v = (*env)->CallFloatMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Float"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(F)Ljava/lang/Float;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + case 'D': { + jdouble v = (*env)->CallDoubleMethodA(env, obj, mid, jargs); + jclass boxCls = (*env)->FindClass(env, "java/lang/Double"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(D)Ljava/lang/Double;"); + result = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, v); + break; + } + default: + result = (*env)->CallObjectMethodA(env, obj, mid, jargs); + break; + } + } + + if (jargs != NULL) free(jargs); + (*env)->ReleaseStringUTFChars(env, methodName, mname); + (*env)->ReleaseStringUTFChars(env, methodDesc, mdesc); + + /* 如果方法执行中抛出异常,清除并返回 null */ + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + return NULL; + } + + return result; +} + +/* ================================================================== */ +/* JNI_OnLoad — dynamic registration via system property */ +/* */ +/* The Kotlin side sets "incision.jvmti.class" to the fully-qualified */ +/* (possibly relocated) class name BEFORE calling System.load(). */ +/* We read it here, find the class, and RegisterNatives all methods. */ +/* ================================================================== */ + +static JNINativeMethod g_methods[] = { + { "nInit", "(Ljava/lang/Class;)Z", (void *)nInit }, + { "nRetransform", "(Ljava/lang/Class;)Z", (void *)nRetransform }, + { "nAvailable", "()Z", (void *)nAvailable }, + { "nDispose", "()V", (void *)nDispose }, + { "nDefineClass", "(Ljava/lang/ClassLoader;Ljava/lang/String;[B)Ljava/lang/Class;", (void *)nDefineClass }, + /* 字节码缓存与抽取相关 */ + { "nCacheOriginal", "(Ljava/lang/String;[B)Z", (void *)native_cache_original }, + { "nGetCachedOriginal", "(Ljava/lang/String;)[B", (void *)native_get_cached }, + { "nExtractClassBytes", "(Ljava/lang/Class;)[B", (void *)native_extract_bytes }, + { "nPurgeCache", "(Ljava/lang/String;)V", (void *)native_purge_cache }, + /* 通用字段/方法访问器 — 绕过访问控制 */ + { "nFieldGet", "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", (void *)native_field_get }, + { "nFieldSet", "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", (void *)native_field_set }, + { "nStaticFieldGet", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", (void *)native_static_field_get }, + { "nStaticFieldSet", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", (void *)native_static_field_set }, + { "nInvokeMethod", "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;", (void *)native_invoke_method }, +}; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + g_jvm = vm; + + JNIEnv *jni = NULL; + if ((*vm)->GetEnv(vm, (void **)&jni, JNI_VERSION_1_6) != JNI_OK || jni == NULL) { + return JNI_VERSION_1_6; + } + + /* Read system property "incision.jvmti.class" */ + jclass sysCls = (*jni)->FindClass(jni, "java/lang/System"); + if (sysCls == NULL) return JNI_VERSION_1_6; + + jmethodID getProp = (*jni)->GetStaticMethodID(jni, sysCls, + "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"); + if (getProp == NULL) return JNI_VERSION_1_6; + + jstring key = (*jni)->NewStringUTF(jni, "incision.jvmti.class"); + if (key == NULL) return JNI_VERSION_1_6; + + jstring val = (jstring)(*jni)->CallStaticObjectMethod(jni, sysCls, getProp, key); + (*jni)->DeleteLocalRef(jni, key); + + if (val == NULL || (*jni)->ExceptionCheck(jni)) { + if ((*jni)->ExceptionCheck(jni)) (*jni)->ExceptionClear(jni); + return JNI_VERSION_1_6; + } + + const char *className = (*jni)->GetStringUTFChars(jni, val, NULL); + if (className == NULL) return JNI_VERSION_1_6; + + /* Convert dots to slashes for JNI FindClass */ + size_t len = strlen(className); + char *jniName = (char *)malloc(len + 1); + if (jniName == NULL) { + (*jni)->ReleaseStringUTFChars(jni, val, className); + return JNI_VERSION_1_6; + } + for (size_t i = 0; i < len; i++) { + jniName[i] = (className[i] == '.') ? '/' : className[i]; + } + jniName[len] = '\0'; + + (*jni)->ReleaseStringUTFChars(jni, val, className); + + jclass target = (*jni)->FindClass(jni, jniName); + free(jniName); + + if (target == NULL) { + if ((*jni)->ExceptionCheck(jni)) (*jni)->ExceptionClear(jni); + return JNI_VERSION_1_6; + } + + (*jni)->RegisterNatives(jni, target, g_methods, + sizeof(g_methods) / sizeof(g_methods[0])); + + if ((*jni)->ExceptionCheck(jni)) { + (*jni)->ExceptionClear(jni); + } + + (*jni)->DeleteLocalRef(jni, target); + (*jni)->DeleteLocalRef(jni, val); + (*jni)->DeleteLocalRef(jni, sysCls); + + return JNI_VERSION_1_6; +} diff --git a/module/incision/src/main/c/include/darwin/jni_md.h b/module/incision/src/main/c/include/darwin/jni_md.h new file mode 100644 index 000000000..6e3708611 --- /dev/null +++ b/module/incision/src/main/c/include/darwin/jni_md.h @@ -0,0 +1,14 @@ +#ifndef _JAVASOFT_JNI_MD_H_ +#define _JAVASOFT_JNI_MD_H_ + +#ifndef JNIEXPORT + #define JNIEXPORT __attribute__((visibility("default"))) +#endif +#define JNIIMPORT +#define JNICALL + +typedef int jint; +typedef long long jlong; +typedef signed char jbyte; + +#endif diff --git a/module/incision/src/main/c/include/linux/jni_md.h b/module/incision/src/main/c/include/linux/jni_md.h new file mode 100644 index 000000000..4cc3ff18e --- /dev/null +++ b/module/incision/src/main/c/include/linux/jni_md.h @@ -0,0 +1,18 @@ +#ifndef _JAVASOFT_JNI_MD_H_ +#define _JAVASOFT_JNI_MD_H_ + +#ifndef JNIEXPORT + #define JNIEXPORT __attribute__((visibility("default"))) +#endif +#define JNIIMPORT +#define JNICALL + +#ifdef _LP64 +typedef int jint; +#else +typedef long jint; +#endif +typedef long long jlong; +typedef signed char jbyte; + +#endif diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java new file mode 100644 index 000000000..a80bf475a --- /dev/null +++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java @@ -0,0 +1,202 @@ +package io.izzel.incision.bridge; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Incision 字节码桥 — 所有被 incision 织入的 INVOKESTATIC 调用都指向这里。 + * + * 设计背景: + * TabooLib Gradle 插件 (io.izzel.taboolib) 的 RelocateRemapper 会在 + * 打包阶段把 taboolib.* 前缀重定向为 <user.group>.taboolib.*。 + * 如果桥类放在 taboolib.module.incision.*,每个插件最终 jar 中的桥 + * 会变成不同类,无法跨插件共享。 + * + * 因此桥必须位于 taboolib.* 之外 的包(本类位于 io.izzel.incision.bridge), + * 并且用 Java 编写以避免 Kotlin 运行时依赖(Kotlin 同样会被 kotlin.* 重定向)。 + * + * 解析顺序: + *
    + *
  1. 优先查系统 ClassLoader 上的 io.izzel.incision.bridge.IncisionGateHost + * (由 GateBootstrapper 通过 appendToSystemClassLoaderSearch 推入), + * 拿到之后所有插件共享同一宿主;
  2. + *
  3. 若系统 ClassLoader 上没有宿主,退化为当前调用方 ClassLoader 的本地 TheatreDispatcher + * (单插件场景正常工作)。
  4. + *
+ * + * 本类通过反射跨 ClassLoader 调用 dispatch,避免类型一致性问题。 + */ +public final class IncisionBridge { + + private IncisionBridge() {} + + private static final Object BYPASS_MISS = new Object(); + + /** 系统 ClassLoader 上的宿主类(若存在) */ + private static volatile Object systemHost = findSystemHost(); + + /** 宿主的 dispatch 方法反射缓存 */ + private static volatile Method systemDispatch = resolveDispatch(systemHost); + + /** ClassLoader → 本地 TheatreDispatcher.dispatch Method 缓存(单插件 fallback 路径) */ + private static final ConcurrentHashMap localCache = new ConcurrentHashMap(); + + /** 全局回退 — 任何 ClassLoader 都能命中的本地 dispatcher */ + private static volatile Method fallbackLocalDispatch = null; + + /** + * 供 weaver 注入的 INVOKESTATIC 目标。 + * + *
+     * INVOKESTATIC io/izzel/incision/bridge/IncisionBridge.dispatch
+     *   (Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
+     * 
+ * + * @param targetSignature 目标方法签名(编译期常量串) + * @param self this 引用(静态方法时为 null) + * @param args 原方法实参 + * @return advice 链的最终返回值;若为 null 调用方应继续执行原方法 + */ + public static Object dispatch(String targetSignature, Object self, Object[] args) { + // 优先走系统宿主 + Method m = systemDispatch; + Object host = systemHost; + if (m != null && host != null) { + try { + return m.invoke(host, targetSignature, self, args); + } catch (Throwable t) { + // 宿主异常不应阻断原方法 + System.err.println("[Incision][Bridge] system host dispatch failed: " + t); + } + } + + // 本地 fallback — 从调用方 ClassLoader 查 TheatreDispatcher + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) cl = IncisionBridge.class.getClassLoader(); + Method local = resolveLocalDispatch(cl); + if (local != null) { + try { + if (local.getParameterCount() == 4) { + return local.invoke(null, targetSignature, self, args, null); + } else { + return local.invoke(null, targetSignature, self, args); + } + } catch (Throwable t) { + System.err.println("[Incision][Bridge] local dispatch failed: " + t); + } + } + return null; + } + + public static Object dispatchBypass(String targetSignature, Object self, Object[] args) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) cl = IncisionBridge.class.getClassLoader(); + Method local = resolveLocalSibling(cl, "dispatchBypass"); + if (local != null) { + try { + return local.invoke(null, targetSignature, self, args); + } catch (Throwable t) { + System.err.println("[Incision][Bridge] local bypass dispatch failed: " + t); + } + } + return BYPASS_MISS; + } + + public static Object bypassMiss() { + return BYPASS_MISS; + } + + public static boolean isBypassMiss(Object value) { + return value == BYPASS_MISS; + } + + /** 宿主绑定入口 — GateBootstrapper 创建 host 后调用此方法完成注册 */ + public static synchronized void bindSystemHost(Object host) { + systemHost = host; + systemDispatch = resolveDispatch(host); + } + + public static synchronized void unbindSystemHost() { + systemHost = null; + systemDispatch = null; + } + + public static boolean hasSystemHost() { + return systemHost != null && systemDispatch != null; + } + + // ----------------------------------------------------------------- + + private static Object findSystemHost() { + try { + ClassLoader sys = ClassLoader.getSystemClassLoader(); + Class hostCls = Class.forName("io.izzel.incision.bridge.IncisionGateHost", true, sys); + // 约定:IncisionGateHost.INSTANCE 是公开静态字段 + return hostCls.getField("INSTANCE").get(null); + } catch (Throwable ignored) { + return null; + } + } + + private static Method resolveDispatch(Object host) { + if (host == null) return null; + try { + return host.getClass().getMethod( + "dispatch", String.class, Object.class, Object[].class + ); + } catch (Throwable t) { + return null; + } + } + + private static Method resolveLocalDispatch(ClassLoader cl) { + Method cached = localCache.get(cl); + if (cached != null) return cached; + // 全局回退 — ClassLoader 不匹配时使用 registerLocalDispatcher 注册的方法 + return fallbackLocalDispatch; + } + + /** 由 IncisionBootstrap 在 CONST 阶段调用,显式注册经过重定向后的 dispatcher 类 */ + public static void registerLocalDispatcher(Class dispatcherClass) { + if (dispatcherClass == null) return; + Method best = pickDispatchMethod(dispatcherClass); + if (best != null) { + localCache.put(dispatcherClass.getClassLoader(), best); + fallbackLocalDispatch = best; + } + } + + /** 借鉴 MeteorInjector 的 getMethod(clazz, returnType, index) 风格 — 形状优先,名字次之 */ + private static Method pickDispatchMethod(Class cls) { + Method named3 = null, named4 = null, anyShape3 = null, anyShape4 = null; + for (Method m : cls.getMethods()) { + Class[] pt = m.getParameterTypes(); + boolean shape3 = pt.length == 3 && pt[0] == String.class && pt[1] == Object.class && pt[2] == Object[].class; + boolean shape4 = pt.length == 4 && pt[0] == String.class && pt[1] == Object.class && pt[2] == Object[].class; + if (m.getName().equals("dispatch")) { + if (shape3) named3 = m; + else if (shape4) named4 = m; + } + if (anyShape3 == null && shape3) anyShape3 = m; + if (anyShape4 == null && shape4) anyShape4 = m; + } + if (named4 != null) return named4; + if (named3 != null) return named3; + return anyShape4 != null ? anyShape4 : anyShape3; + } + + private static Method resolveLocalSibling(ClassLoader cl, String methodName) { + Method local = resolveLocalDispatch(cl); + if (local == null) return null; + try { + return local.getDeclaringClass().getMethod(methodName, String.class, Object.class, Object[].class); + } catch (Throwable ignored) { + return null; + } + } + + /** 插件 DISABLE 时调用 — 移除该 ClassLoader 关联的本地 dispatcher 缓存 */ + public static void unregisterLocalDispatcher(ClassLoader cl) { + if (cl != null) localCache.remove(cl); + } +} diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionGateHost.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionGateHost.java new file mode 100644 index 000000000..60b78a457 --- /dev/null +++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionGateHost.java @@ -0,0 +1,107 @@ +package io.izzel.incision.bridge; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 系统级 IncisionGate 宿主 — 由 GateBootstrapper 通过 + * Instrumentation.appendToSystemClassLoaderSearch 推入系统 ClassLoader。 + * + * 设计: + * - 仅持有 target → List<AdviceProxy> 链表 + * - AdviceProxy 是反射代理对象,本类通过反射调用其 invoke, + * 不需要类型一致(避免跨 ClassLoader 类型问题) + * - 公开 INSTANCE 静态字段供 IncisionBridge 反射拿取 + * + * 不依赖 Kotlin、不依赖 taboolib 包名 — 因此在系统 ClassLoader 上加载安全。 + */ +public final class IncisionGateHost { + + public static final IncisionGateHost INSTANCE = new IncisionGateHost(); + + /** target signature → advice proxies */ + private final ConcurrentHashMap> chains = new ConcurrentHashMap>(); + + /** AdviceProxy class → invoke Method 缓存 */ + private final ConcurrentHashMap, Method> invokeCache = new ConcurrentHashMap, Method>(); + + private IncisionGateHost() {} + + /** advice 注册:proxy 必须有 invoke(String, Object, Object[]) 方法 */ + public void register(String targetSignature, Object adviceProxy) { + chains.computeIfAbsent(targetSignature, new java.util.function.Function>() { + @Override public CopyOnWriteArrayList apply(String s) { return new CopyOnWriteArrayList(); } + }).add(adviceProxy); + sortByPriority(targetSignature); + } + + public boolean unregister(String targetSignature, Object adviceProxy) { + CopyOnWriteArrayList list = chains.get(targetSignature); + return list != null && list.remove(adviceProxy); + } + + public Object dispatch(String targetSignature, Object self, Object[] args) { + CopyOnWriteArrayList list = chains.get(targetSignature); + if (list == null || list.isEmpty()) return null; + Object result = null; + for (Object proxy : list) { + try { + Method m = invokeCache.get(proxy.getClass()); + if (m == null) { + m = proxy.getClass().getMethod("invoke", String.class, Object.class, Object[].class); + invokeCache.put(proxy.getClass(), m); + } + Object r = m.invoke(proxy, targetSignature, self, args); + if (r != null) result = r; + } catch (Throwable t) { + System.err.println("[Incision][Host] advice invoke failed: " + t); + } + } + return result; + } + + public List listTargets() { + return new ArrayList(chains.keySet()); + } + + public int healByClassLoader(ClassLoader cl) { + int n = 0; + for (CopyOnWriteArrayList list : chains.values()) { + List rm = new ArrayList(); + for (Object p : list) { + ClassLoader pcl = p.getClass().getClassLoader(); + if (pcl == cl) rm.add(p); + } + n += rm.size(); + list.removeAll(rm); + } + return n; + } + + private void sortByPriority(String targetSignature) { + CopyOnWriteArrayList list = chains.get(targetSignature); + if (list == null || list.size() < 2) return; + List snapshot = new ArrayList(list); + snapshot.sort(new java.util.Comparator() { + @Override public int compare(Object a, Object b) { + return Integer.compare(priorityOf(b), priorityOf(a)); + } + }); + list.clear(); + list.addAll(snapshot); + } + + private int priorityOf(Object proxy) { + try { + Method m = proxy.getClass().getMethod("priority"); + return ((Number) m.invoke(proxy)).intValue(); + } catch (Throwable t) { + return 0; + } + } + + public int apiVersion() { return 1; } +} diff --git a/module/incision/src/main/java/taboolib/module/incision/loader/attach/IncisionSelfAgent.java b/module/incision/src/main/java/taboolib/module/incision/loader/attach/IncisionSelfAgent.java new file mode 100644 index 000000000..847fe21a4 --- /dev/null +++ b/module/incision/src/main/java/taboolib/module/incision/loader/attach/IncisionSelfAgent.java @@ -0,0 +1,40 @@ +package taboolib.module.incision.loader.attach; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Field; + +public final class IncisionSelfAgent { + + private IncisionSelfAgent() {} + + public static void premain(String args, Instrumentation inst) { inject(args, inst); } + public static void agentmain(String args, Instrumentation inst) { inject(args, inst); } + + private static void inject(String targetClassName, Instrumentation inst) { + // targetClassName is passed from ManualSelfAttach as the actual (possibly relocated) class name + if (targetClassName != null && !targetClassName.isEmpty()) { + for (Class cls : inst.getAllLoadedClasses()) { + if (cls.getName().equals(targetClassName)) { + if (trySetField(cls, inst)) return; + } + } + } + // fallback: search by simple name suffix + for (Class cls : inst.getAllLoadedClasses()) { + if (cls.getName().endsWith(".loader.attach.ManualSelfAttach")) { + if (trySetField(cls, inst)) return; + } + } + } + + private static boolean trySetField(Class cls, Instrumentation inst) { + try { + Field f = cls.getDeclaredField("captured"); + f.setAccessible(true); + f.set(null, inst); + return true; + } catch (Throwable ignored) { + return false; + } + } +} diff --git a/module/incision/src/main/java/taboolib/module/incision/pred/PredOps.java b/module/incision/src/main/java/taboolib/module/incision/pred/PredOps.java new file mode 100644 index 000000000..5bb9471b0 --- /dev/null +++ b/module/incision/src/main/java/taboolib/module/incision/pred/PredOps.java @@ -0,0 +1,260 @@ +package taboolib.module.incision.pred; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * 谓词运行时通用算子(被 PredCompiler 生成的字节码 INVOKESTATIC 进来)。 + * + *

放在 java 包以避免 Kotlin metadata / 重定位影响;所有方法的签名都使用 Object/boolean + * 等基础类型,便于 ASM 在不知道实际运行期类型的情况下生成调用。 + * + *

语义见 PredAst 注释;此类不应抛 RuntimeException 给字节码,错误一律转为 false 或 + * 通过 throw Trauma.Predicate.* 由上层 dispatcher 包装。 + */ +public final class PredOps { + + private PredOps() {} + + // ---- 比较 ---- + + public static boolean eq(Object a, Object b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (a instanceof Number && b instanceof Number) { + return ((Number) a).doubleValue() == ((Number) b).doubleValue(); + } + return a.equals(b); + } + + public static boolean neq(Object a, Object b) { return !eq(a, b); } + + public static boolean lt(Object a, Object b) { return a != null && b != null && cmp(a, b) < 0; } + public static boolean gt(Object a, Object b) { return a != null && b != null && cmp(a, b) > 0; } + public static boolean le(Object a, Object b) { return a != null && b != null && cmp(a, b) <= 0; } + public static boolean ge(Object a, Object b) { return a != null && b != null && cmp(a, b) >= 0; } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static int cmp(Object a, Object b) { + if (a == null || b == null) return 0; // 与 null 比较一律视为 0,由 eq/neq 单独处理 + if (a instanceof Number && b instanceof Number) { + double da = ((Number) a).doubleValue(); + double db = ((Number) b).doubleValue(); + return Double.compare(da, db); + } + if (a instanceof Comparable && a.getClass().isInstance(b)) { + return ((Comparable) a).compareTo(b); + } + return 0; + } + + public static boolean matches(Object a, Object regex) { + if (a == null || regex == null) return false; + Pattern p = patternCache.computeIfAbsent(regex.toString(), Pattern::compile); + return p.matcher(a.toString()).matches(); + } + + private static final ConcurrentHashMap patternCache = new ConcurrentHashMap(); + + /** `expr in collection` */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static boolean contains(Object element, Object collection) { + if (collection == null) return false; + if (collection instanceof java.util.Collection) { + return ((java.util.Collection) collection).contains(element); + } + if (collection instanceof Map) { + return ((Map) collection).containsKey(element); + } + if (collection instanceof Object[]) { + for (Object o : (Object[]) collection) { + if (eq(o, element)) return true; + } + return false; + } + if (collection instanceof CharSequence && element != null) { + return collection.toString().contains(element.toString()); + } + return false; + } + + // ---- 类型算子 ---- + + public static boolean isInstanceOf(Object x, Class t) { + return x != null && t != null && t.isInstance(x); + } + + /** ic:INSTANCEOF + 不等 class(即子类型且不是同一个类) */ + public static boolean isInstanceChild(Object x, Class t) { + if (x == null || t == null) return false; + return t.isInstance(x) && x.getClass() != t; + } + + /** ip:T.isAssignableFrom(x.getClass())(与 isInstance 类似,但 x 为 Class 时检查 Class 继承) */ + public static boolean isAssignable(Object x, Class t) { + if (x == null || t == null) return false; + Class xc = (x instanceof Class) ? (Class) x : x.getClass(); + return t.isAssignableFrom(xc); + } + + /** it:x.getClass() == T */ + public static boolean isExactType(Object x, Class t) { + return x != null && t != null && x.getClass() == t; + } + + /** as:失败返回 null,谓词层将 null 视为 false。 */ + public static Object asCast(Object x, Class t) { + if (x == null || t == null) return null; + return t.isInstance(x) ? x : null; + } + + // ---- 成员访问 ---- + + /** + * 反射读取属性 / Kotlin 属性 / get/is 前缀方法。 + * 对应 PropertyAccess。失败一律返回 null(由谓词语义层决定 false / 短路)。 + */ + public static Object getProperty(Object receiver, String name) { + if (receiver == null) return null; + Class c = receiver.getClass(); + // 字段 + try { + Field f = c.getField(name); + return f.get(receiver); + } catch (NoSuchFieldException ignore) { + } catch (Throwable ignore) { return null; } + // getXxx + String capitalized = name.length() == 0 ? name : + Character.toUpperCase(name.charAt(0)) + name.substring(1); + try { + Method m = c.getMethod("get" + capitalized); + return m.invoke(receiver); + } catch (NoSuchMethodException ignore) { + } catch (Throwable ignore) { return null; } + try { + Method m = c.getMethod("is" + capitalized); + return m.invoke(receiver); + } catch (NoSuchMethodException ignore) { + } catch (Throwable ignore) { return null; } + // 直接同名(Kotlin 字段反射不到也走这里) + try { + Method m = c.getMethod(name); + return m.invoke(receiver); + } catch (NoSuchMethodException ignore) { + } catch (Throwable ignore) { return null; } + return null; + } + + /** + * 反射调用方法。args 为可变参数 Object 数组。 + * 简化匹配:按形参个数定位,若多个候选则按类型兼容性挑第一个;否则 null。 + */ + public static Object callMethod(Object receiver, String name, Object[] args) { + if (receiver == null) return null; + Class c = receiver.getClass(); + Method best = null; + Method[] all = c.getMethods(); + for (Method m : all) { + if (!m.getName().equals(name)) continue; + if (m.getParameterCount() != (args == null ? 0 : args.length)) continue; + if (matchesParams(m, args)) { best = m; break; } + if (best == null) best = m; // arity 匹配的兜底 + } + if (best == null) return null; + try { return best.invoke(receiver, args); } + catch (Throwable t) { return null; } + } + + private static boolean matchesParams(Method m, Object[] args) { + Class[] ps = m.getParameterTypes(); + if (args == null) return ps.length == 0; + for (int i = 0; i < ps.length; i++) { + Object a = args[i]; + if (a == null) { + if (ps[i].isPrimitive()) return false; + continue; + } + Class p = box(ps[i]); + if (!p.isInstance(a)) return false; + } + return true; + } + + private static Class box(Class p) { + if (!p.isPrimitive()) return p; + if (p == int.class) return Integer.class; + if (p == long.class) return Long.class; + if (p == double.class) return Double.class; + if (p == float.class) return Float.class; + if (p == boolean.class) return Boolean.class; + if (p == byte.class) return Byte.class; + if (p == char.class) return Character.class; + if (p == short.class) return Short.class; + return p; + } + + /** 下标访问:List / Map / Object[] / CharSequence。 */ + public static Object index(Object receiver, Object key) { + if (receiver == null) return null; + if (receiver instanceof List) { + int i = ((Number) key).intValue(); + List l = (List) receiver; + return (i >= 0 && i < l.size()) ? l.get(i) : null; + } + if (receiver instanceof Map) { + return ((Map) receiver).get(key); + } + if (receiver instanceof Object[]) { + int i = ((Number) key).intValue(); + Object[] arr = (Object[]) receiver; + return (i >= 0 && i < arr.length) ? arr[i] : null; + } + if (receiver instanceof CharSequence) { + int i = ((Number) key).intValue(); + CharSequence s = (CharSequence) receiver; + return (i >= 0 && i < s.length()) ? Character.valueOf(s.charAt(i)) : null; + } + return null; + } + + // ---- 真值化 ---- + + /** Object → boolean:null/false → false,其余 true(数字 0 仍按真)。 */ + public static boolean truthy(Object x) { + if (x == null) return false; + if (x instanceof Boolean) return ((Boolean) x).booleanValue(); + return true; + } + + /** 把 boolean 装回 Object,方便 cmp/and/or 中转。 */ + public static Object box(boolean b) { return Boolean.valueOf(b); } + + // ---- 类型查找(缓存) ---- + + private static final ConcurrentHashMap> classCache = new ConcurrentHashMap>(); + + public static Class resolveType(String name, ClassLoader cl) { + Class c = classCache.get(name); + if (c != null) return c; + ClassLoader[] loaders = new ClassLoader[] { + cl, + Thread.currentThread().getContextClassLoader(), + PredOps.class.getClassLoader(), + ClassLoader.getSystemClassLoader(), + }; + for (ClassLoader loader : loaders) { + if (loader == null) continue; + try { + c = Class.forName(name, false, loader); + classCache.put(name, c); + return c; + } catch (Throwable ignored) { + } + } + return null; + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt new file mode 100644 index 000000000..51a76971f --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt @@ -0,0 +1,210 @@ +package taboolib.module.incision + +import io.izzel.incision.bridge.IncisionBridge +import taboolib.common.Inject +import taboolib.common.LifeCycle +import taboolib.common.env.RuntimeDependencies +import taboolib.common.env.RuntimeDependency +import taboolib.common.platform.Awake +import taboolib.module.incision.diagnostic.Checkup +import taboolib.module.incision.diagnostic.ConflictAnalyzer +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.gate.IncisionGateLocator +import taboolib.module.incision.lifecycle.AutoHealHandler +import taboolib.module.incision.loader.InstrumentationBackend +import taboolib.module.incision.loader.JvmtiBackend +import taboolib.module.incision.loader.PipelineBackend +import taboolib.module.incision.reflex.IncisionReflex +import taboolib.module.incision.remap.RemapRouter +import taboolib.module.incision.remap.TabooLibNmsResolver +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher + +/** + * Incision 模块入口。 + * + * - CONST:模块对象实例化时先准备织入基础设施(resolver / bridge / pipeline / reflex adapter), + * 随后由 [taboolib.module.incision.loader.SurgeonScanner] 在同一生命周期内完成扫描与物理织入。 + * - INIT / LOAD:不再承担核心织入动作,留给宿主插件自身生命周期逻辑。 + * - ENABLE:接入 IncisionGate(系统级网关),输出 checkup / 冲突分析 / 后端状态。 + * + * 该入口仅做"调度与汇总",具体逻辑分散到 SurgeryRegistry / Checkup / Scalpel 等组件。 + */ +@Awake +@Inject +object IncisionBootstrap { + + /** 当前 incision API 版本,用于网关版本协商 */ + const val API_VERSION = 1 + + init { + prepareConst() + } + + private fun prepareConst() { + // 0. asm-tree 运行时可用性自检(PrimitiveLoader bootstrap 新增) + probeAsmTree() + // 1. 尝试接入 TabooLib 的 NMS resolver(若 :module:bukkit-nms 在 classpath) + TabooLibNmsResolver.installIfAvailable() + // 2. 安装反射穿透适配器,让 TabooLib reflex 在 invoke 时自动 withoutIncision + IncisionReflex.installReflexAdapter() + // 3. 把经 gradle 重定位后真实的 TheatreDispatcher 类名注册进桥(插件 CL 版本) + try { + IncisionBridge.registerLocalDispatcher(TheatreDispatcher::class.java) + } catch (t: Throwable) { + Forensics.warn("Incision bridge registerLocalDispatcher failed: ${t.message}") + } + // 4. 注入 IncisionBridge 到 bootstrap ClassLoader,使跨 CL 目标(NMS、Bukkit API)能解析桥类 + // 必须在任何 installWeaver/retransform 之前完成 + injectBridgeIntoSystemClassLoader() + Forensics.info("Incision CONST: api=$API_VERSION resolver=${RemapRouter.name()}") + // 5. 接入 TabooLib NMSProxy 管线末端(PipelineBackend) + PipelineBackend.installIntoTabooLib() + } + + @Awake(LifeCycle.ENABLE) + fun onEnable() { + Forensics.info("Incision ENABLE: gate / 诊断收尾开始") + // 1. 接入 / 创建 IncisionGate + try { + val gate = IncisionGateLocator.locateOrCreate(API_VERSION) + Forensics.info("Incision Gate online: api=${gate.apiVersion()}") + } catch (t: Throwable) { + Forensics.warn("Incision Gate 接入失败(将使用本地 dispatcher 兜底):${t.javaClass.name}: ${t.message}") + if (Forensics.DEBUG) t.printStackTrace() + } + // 2. 输出已注册切术的 startup checkup(此时 CONST 阶段的 @Surgeon 已完成注册) + Checkup.runStartupCheckup() + // 3. 跑全量冲突分析 + for (r in ConflictAnalyzer.analyzeAll()) { + ConflictAnalyzer.emit(r) + } + // 4. 输出 retransform 后端状态:优先 Instrumentation,回退到 JVMTI native + if (InstrumentationBackend.available()) { + Forensics.info("Instrumentation 后端就绪") + } else if (JvmtiBackend.available()) { + Forensics.info("JVMTI native 后端就绪(Instrumentation 不可用)") + } else { + Forensics.warn("Instrumentation / JVMTI 均不可用 — 仅 Pipeline / ClassLoaderHook 路径可用") + } + } + + @Awake(LifeCycle.DISABLE) + fun onDisable() { + Forensics.info("Incision DISABLE: 卸载本插件全部 advice") + // 卸载当前 ClassLoader 下所有 advice + AutoHealHandler.healByClassLoader(IncisionBootstrap::class.java.classLoader) + SurgeryRegistry.list().toList().forEach { it.heal() } + // 解绑本插件在桥上的本地 dispatcher 缓存 + runCatching { + IncisionBridge.unregisterLocalDispatcher(IncisionBootstrap::class.java.classLoader) + } + } + + private fun probeAsmTree() { + val cl = IncisionBootstrap::class.java.classLoader + val probes = listOf( + "org.objectweb.asm.tree.ClassNode", + "org.objectweb.asm.tree.MethodNode", + "org.objectweb.asm.tree.InsnList" + ) + val failed = mutableListOf() + for (name in probes) { + try { + Class.forName(name, false, cl) + } catch (t: Throwable) { + failed += "$name(${t.javaClass.simpleName})" + } + } + if (failed.isNotEmpty()) { + Forensics.warn("asm-tree 不可用 — 缺失: ${failed.joinToString(", ")}") + return + } + if (!Forensics.DEBUG) return + val asmCoreLoc = runCatching { + Class.forName("org.objectweb.asm.ClassWriter", false, cl) + .protectionDomain?.codeSource?.location?.toString() + }.getOrNull() ?: "?" + val asmTreeLoc = runCatching { + Class.forName("org.objectweb.asm.tree.ClassNode", false, cl) + .protectionDomain?.codeSource?.location?.toString() + }.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() } + if (bytes != null) { + val node = org.objectweb.asm.tree.ClassNode() + org.objectweb.asm.ClassReader(bytes).accept(node, 0) + Forensics.debug("asm-tree probe OK: name=${node.name} methods=${node.methods.size}") + } + } catch (t: Throwable) { + Forensics.debug("asm-tree probe: tree 读写测试失败 ${t.javaClass.name}: ${t.message}") + } + } + + private fun injectBridgeIntoSystemClassLoader() { + val bridgeClassName = "io.izzel.incision.bridge.IncisionBridge" + val bridgeInternalName = bridgeClassName.replace('.', '/') + val sysCL = ClassLoader.getSystemClassLoader() + // 已存在于 bootstrap 或 system CL — 只需注册 dispatcher + val existing = try { + Class.forName(bridgeClassName, true, sysCL) + } catch (_: Throwable) { + null + } + if (existing != null && existing.classLoader == null) { + Forensics.info("IncisionBridge 已存在于 bootstrap ClassLoader") + registerDispatcherOn(existing) + return + } + val resourcePath = "$bridgeInternalName.class" + val bytes = IncisionBootstrap::class.java.classLoader.getResourceAsStream(resourcePath)?.use { it.readBytes() } + if (bytes == null) { + Forensics.warn("IncisionBridge.class 资源未找到: $resourcePath") + return + } + // 路径 1: JVMTI native — defineClass 直接注入 bootstrap CL + if (JvmtiBackend.available()) { + val cls = JvmtiBackend.defineClassInClassLoader(null, bridgeClassName, bytes) + if (cls != null) { + Forensics.info("IncisionBridge 已注入 bootstrap ClassLoader (JVMTI)") + registerDispatcherOn(cls) + return + } + } + // 路径 2: Instrumentation — 打包临时 jar 并 appendToBootstrapClassLoaderSearch + val inst = InstrumentationBackend.ensure() + if (inst != null) { + try { + val jar = java.io.File.createTempFile("incision-bridge-", ".jar").apply { deleteOnExit() } + val manifest = java.util.jar.Manifest().apply { + mainAttributes[java.util.jar.Attributes.Name.MANIFEST_VERSION] = "1.0" + } + java.util.jar.JarOutputStream(java.io.FileOutputStream(jar), manifest).use { out -> + out.putNextEntry(java.util.jar.JarEntry("$bridgeInternalName.class")) + out.write(bytes) + out.closeEntry() + } + inst.appendToBootstrapClassLoaderSearch(java.util.jar.JarFile(jar)) + val cls = Class.forName(bridgeClassName, true, sysCL) + Forensics.info("IncisionBridge 已注入 bootstrap ClassLoader (Instrumentation)") + registerDispatcherOn(cls) + return + } catch (t: Throwable) { + Forensics.warn("IncisionBridge Instrumentation 注入失败: ${t.javaClass.name}: ${t.message}") + } + } + // 路径 3: fallback — 仅在插件 CL 中,跨 CL 目标无法使用 + Forensics.warn("IncisionBridge 未能注入 bootstrap/system CL — 跨 ClassLoader 目标(NMS、Bukkit API)将不可用") + } + + private fun registerDispatcherOn(bridgeClass: Class<*>) { + try { + val regMethod = bridgeClass.getMethod("registerLocalDispatcher", Class::class.java) + regMethod.invoke(null, TheatreDispatcher::class.java) + Forensics.info("IncisionBridge dispatcher 已注册 (CL=${bridgeClass.classLoader ?: "bootstrap"})") + } catch (t: Throwable) { + Forensics.warn("IncisionBridge registerLocalDispatcher 失败: ${t.message}") + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Bypass.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Bypass.kt new file mode 100644 index 000000000..9246ce315 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Bypass.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.annotation + +/** + * 重定向 advice。 + * + * 用途: + * 把目标方法中的单条调用指令替换成自定义 handler,语义上接近 Mixin `@Redirect`。 + * + * 使用: + * 通过 [method] 指定宿主方法,通过 [site] 指定要替换的调用点或字段访问点。 + * handler 需要和被替换的调用位点在参数与返回值语义上保持兼容。 + * + * 效果: + * 命中后原位点不会再执行原始调用,而是改为执行你的 handler。 + * + * 局限: + * 1. 它只替换一个中段位点,不负责整个方法体的控制流接管。 + * 2. 如果 [site] 选得不精确,可能出现未命中或误命中的问题。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Bypass( + + /** 宿主方法描述符,格式为 `类#方法名(参数)返回`。 */ + val method: String, + + /** 被替换的目标指令锚点。 */ + val site: Site, + + /** 额外的指令序列约束,用于在同一宿主方法中进一步缩小命中范围。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Excise.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Excise.kt new file mode 100644 index 000000000..2bbeec8ae --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Excise.kt @@ -0,0 +1,32 @@ +package taboolib.module.incision.annotation + +/** + * 覆写式 advice。 + * + * 用途: + * 直接替换整段目标方法,等价于 Mixin `@Overwrite`。 + * + * 使用: + * 把注解标在 `@Surgeon` object 的方法上,通过 [scope] 指定唯一目标。 + * handler 签名应与目标方法兼容,包括 self 与原始参数。 + * + * 效果: + * 命中后原方法体不再执行,所有行为由 handler 接管。 + * + * 局限: + * 1. 同一目标只允许一个 [Excise],多于一个会触发 `Trauma.Conflict.MultipleExcise`。 + * 2. 因为直接替换整段方法,兼容性和风险都高于入口/出口类 advice。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Excise( + /** 目标选择器 DSL。 */ + val scope: String, + + /** 额外的指令序列约束,用于限制只对满足特定字节码形态的目标方法生效。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Graft.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Graft.kt new file mode 100644 index 000000000..e60dea677 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Graft.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.annotation + +/** + * 植入式 advice。 + * + * 用途: + * 在指定锚点前后追加一段逻辑,而不替换原始指令,适合做探针、记录和轻量联动。 + * + * 使用: + * 通过 [method] 锁定宿主方法,通过 [site] 选择要植入的位置,例如 INVOKE、FIELD_GET、 + * FIELD_PUT、NEW 等。 + * + * 效果: + * 命中后会在选定位置额外插入一次 handler 调用,原始指令仍会继续执行。 + * + * 局限: + * 1. `@Graft` 擅长“追加”,不负责替换或阻断原调用。 + * 2. 锚点选择错误时很容易出现未命中或执行次数偏差。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Graft( + + /** 宿主方法描述符,格式为 `类#方法名(参数)返回`。 */ + val method: String, + + /** 植入位置。 */ + val site: Site, + + /** 额外的指令序列约束,用于在同一宿主方法中进一步缩小命中范围。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/InsnPattern.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/InsnPattern.kt new file mode 100644 index 000000000..d752c7ea6 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/InsnPattern.kt @@ -0,0 +1,26 @@ +package taboolib.module.incision.annotation + +/** + * 指令序列模式。 + * + * 用途: + * 用一组 [Step] 描述“目标方法里必须出现怎样的字节码片段”,从而把匹配从方法级进一步收紧到 + * 特定字节码形态。 + * + * 使用: + * 把它填到各类 advice 的 `pattern` 参数中;如果还想在 DSL 或诊断里引用该模式, + * 可以搭配 `where` 给它起一个稳定名字。 + * + * 效果: + * 运行时会按顺序匹配 [steps],只有整段序列都满足时,目标方法才会被视为命中。 + * + * 局限: + * 1. 它面向编译后字节码,不是面向源码;编译器优化、常量折叠都会影响结果。 + * 2. 空 `steps` 表示关闭该约束,而不是“匹配空序列”。 + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class InsnPattern( + /** 顺序匹配的步骤数组;留空表示不启用模式约束。 */ + val steps: Array = [], +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/KotlinTarget.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/KotlinTarget.kt new file mode 100644 index 000000000..9ef974961 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/KotlinTarget.kt @@ -0,0 +1,28 @@ +package taboolib.module.incision.annotation + +/** + * Kotlin 特殊目标扩展声明。 + * + * 用途: + * 当目标来自 Kotlin companion 或 `@JvmStatic` 桥接方法时,显式声明同一 advice + * 是否还要附加到这些额外生成的方法上。 + * + * 使用: + * 把它加在 advice 方法上,并根据需要开启 [companionInstance] 或 [jvmStaticBridge]。 + * + * 效果: + * 扫描阶段会在主 target 之外,再补充 companion 实例方法或静态桥接方法对应的 target。 + * + * 局限: + * 1. 依赖 Kotlin 编译器生成代码的具体形态,升级编译器后应回归测试。 + * 2. 只解决 Kotlin 额外方法映射问题,不负责 remap 或版本过滤。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class KotlinTarget( + /** 是否额外挂到 companion 实例方法上。 */ + val companionInstance: Boolean = false, + /** 是否额外挂到 `@JvmStatic` 生成的静态桥接方法上。 */ + val jvmStaticBridge: Boolean = false, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Lead.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Lead.kt new file mode 100644 index 000000000..09f3489e8 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Lead.kt @@ -0,0 +1,35 @@ +package taboolib.module.incision.annotation + +/** + * 先导 advice。 + * + * 用途: + * 在目标方法真正进入方法体之前插入一段只读或轻量改写逻辑,等价于 + * Spring `@Before` 或 Mixin `@Inject(at = @At("HEAD"))` 的入口切入。 + * + * 使用: + * 把注解标在 `@Surgeon` object 内的方法上,并通过 [scope] 指定要命中的目标。 + * handler 常见签名为 `fun(theatre: Theatre)`,通过 theatre 读取 self、args、上下文信息。 + * + * 效果: + * 命中后会在方法入口处额外触发一次 advice,不会替换原方法,也不会自动中断原流程。 + * 如需控制是否继续执行原方法,应改用 [Splice]。 + * + * 局限: + * 1. 该注解主要表达“入口发生了什么”,不擅长替换中段调用点。 + * 2. 如果同时配合 [pattern],最终是否命中仍取决于编译后的真实字节码形态。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Lead( + + /** 目标选择器 DSL,例如 `method:org.bukkit.entity.Player#kickPlayer(*)`。 */ + val scope: String, + + /** 额外的指令序列约束,用于把入口切入缩小到满足特定字节码形态的目标方法。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Op.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Op.kt new file mode 100644 index 000000000..8cfd72814 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Op.kt @@ -0,0 +1,209 @@ +package taboolib.module.incision.annotation + +import org.objectweb.asm.Opcodes + +/** + * ASM 字节码指令枚举。 + * + * 用途: + * 为 [Step.opcode] 提供稳定、可读的枚举值,避免在模式里直接手写 ASM 整数常量。 + * + * 使用: + * 大多数情况下直接在 `Step(opcode = Op.INVOKEVIRTUAL)` 里引用即可; + * 当需要从整数 opcode 反查时,可使用 [fromInt]。 + * + * 效果: + * 该枚举基本与 [org.objectweb.asm.Opcodes] 一一对应,`ANY` 额外表示“任意指令”。 + * + * 局限: + * 1. 它只表达 opcode 类型,不携带 owner/name/desc 等额外条件。 + * 2. 如果底层指令值来自 ASM 之外的特殊语义,仍需结合实际字节码理解。 + * + * @property opcode 对应的 ASM 指令值;`-1` 为通配([ANY])。 + */ +enum class Op(val opcode: Int) { + + /** 通配:匹配任意指令。 */ + ANY(-1), + + NOP(Opcodes.NOP), + ACONST_NULL(Opcodes.ACONST_NULL), + ICONST_M1(Opcodes.ICONST_M1), + ICONST_0(Opcodes.ICONST_0), + ICONST_1(Opcodes.ICONST_1), + ICONST_2(Opcodes.ICONST_2), + ICONST_3(Opcodes.ICONST_3), + ICONST_4(Opcodes.ICONST_4), + ICONST_5(Opcodes.ICONST_5), + LCONST_0(Opcodes.LCONST_0), + LCONST_1(Opcodes.LCONST_1), + FCONST_0(Opcodes.FCONST_0), + FCONST_1(Opcodes.FCONST_1), + FCONST_2(Opcodes.FCONST_2), + DCONST_0(Opcodes.DCONST_0), + DCONST_1(Opcodes.DCONST_1), + BIPUSH(Opcodes.BIPUSH), + SIPUSH(Opcodes.SIPUSH), + LDC(Opcodes.LDC), + + ILOAD(Opcodes.ILOAD), + LLOAD(Opcodes.LLOAD), + FLOAD(Opcodes.FLOAD), + DLOAD(Opcodes.DLOAD), + ALOAD(Opcodes.ALOAD), + + IALOAD(Opcodes.IALOAD), + LALOAD(Opcodes.LALOAD), + FALOAD(Opcodes.FALOAD), + DALOAD(Opcodes.DALOAD), + AALOAD(Opcodes.AALOAD), + BALOAD(Opcodes.BALOAD), + CALOAD(Opcodes.CALOAD), + SALOAD(Opcodes.SALOAD), + + ISTORE(Opcodes.ISTORE), + LSTORE(Opcodes.LSTORE), + FSTORE(Opcodes.FSTORE), + DSTORE(Opcodes.DSTORE), + ASTORE(Opcodes.ASTORE), + + IASTORE(Opcodes.IASTORE), + LASTORE(Opcodes.LASTORE), + FASTORE(Opcodes.FASTORE), + DASTORE(Opcodes.DASTORE), + AASTORE(Opcodes.AASTORE), + BASTORE(Opcodes.BASTORE), + CASTORE(Opcodes.CASTORE), + SASTORE(Opcodes.SASTORE), + + POP(Opcodes.POP), + POP2(Opcodes.POP2), + DUP(Opcodes.DUP), + DUP_X1(Opcodes.DUP_X1), + DUP_X2(Opcodes.DUP_X2), + DUP2(Opcodes.DUP2), + DUP2_X1(Opcodes.DUP2_X1), + DUP2_X2(Opcodes.DUP2_X2), + SWAP(Opcodes.SWAP), + + IADD(Opcodes.IADD), + LADD(Opcodes.LADD), + FADD(Opcodes.FADD), + DADD(Opcodes.DADD), + ISUB(Opcodes.ISUB), + LSUB(Opcodes.LSUB), + FSUB(Opcodes.FSUB), + DSUB(Opcodes.DSUB), + IMUL(Opcodes.IMUL), + LMUL(Opcodes.LMUL), + FMUL(Opcodes.FMUL), + DMUL(Opcodes.DMUL), + IDIV(Opcodes.IDIV), + LDIV(Opcodes.LDIV), + FDIV(Opcodes.FDIV), + DDIV(Opcodes.DDIV), + IREM(Opcodes.IREM), + LREM(Opcodes.LREM), + FREM(Opcodes.FREM), + DREM(Opcodes.DREM), + INEG(Opcodes.INEG), + LNEG(Opcodes.LNEG), + FNEG(Opcodes.FNEG), + DNEG(Opcodes.DNEG), + ISHL(Opcodes.ISHL), + LSHL(Opcodes.LSHL), + ISHR(Opcodes.ISHR), + LSHR(Opcodes.LSHR), + IUSHR(Opcodes.IUSHR), + LUSHR(Opcodes.LUSHR), + IAND(Opcodes.IAND), + LAND(Opcodes.LAND), + IOR(Opcodes.IOR), + LOR(Opcodes.LOR), + IXOR(Opcodes.IXOR), + LXOR(Opcodes.LXOR), + IINC(Opcodes.IINC), + + I2L(Opcodes.I2L), + I2F(Opcodes.I2F), + I2D(Opcodes.I2D), + L2I(Opcodes.L2I), + L2F(Opcodes.L2F), + L2D(Opcodes.L2D), + F2I(Opcodes.F2I), + F2L(Opcodes.F2L), + F2D(Opcodes.F2D), + D2I(Opcodes.D2I), + D2L(Opcodes.D2L), + D2F(Opcodes.D2F), + I2B(Opcodes.I2B), + I2C(Opcodes.I2C), + I2S(Opcodes.I2S), + + LCMP(Opcodes.LCMP), + FCMPL(Opcodes.FCMPL), + FCMPG(Opcodes.FCMPG), + DCMPL(Opcodes.DCMPL), + DCMPG(Opcodes.DCMPG), + + IFEQ(Opcodes.IFEQ), + IFNE(Opcodes.IFNE), + IFLT(Opcodes.IFLT), + IFGE(Opcodes.IFGE), + IFGT(Opcodes.IFGT), + IFLE(Opcodes.IFLE), + IF_ICMPEQ(Opcodes.IF_ICMPEQ), + IF_ICMPNE(Opcodes.IF_ICMPNE), + IF_ICMPLT(Opcodes.IF_ICMPLT), + IF_ICMPGE(Opcodes.IF_ICMPGE), + IF_ICMPGT(Opcodes.IF_ICMPGT), + IF_ICMPLE(Opcodes.IF_ICMPLE), + IF_ACMPEQ(Opcodes.IF_ACMPEQ), + IF_ACMPNE(Opcodes.IF_ACMPNE), + + GOTO(Opcodes.GOTO), + JSR(Opcodes.JSR), + RET(Opcodes.RET), + TABLESWITCH(Opcodes.TABLESWITCH), + LOOKUPSWITCH(Opcodes.LOOKUPSWITCH), + + IRETURN(Opcodes.IRETURN), + LRETURN(Opcodes.LRETURN), + FRETURN(Opcodes.FRETURN), + DRETURN(Opcodes.DRETURN), + ARETURN(Opcodes.ARETURN), + RETURN(Opcodes.RETURN), + + GETSTATIC(Opcodes.GETSTATIC), + PUTSTATIC(Opcodes.PUTSTATIC), + GETFIELD(Opcodes.GETFIELD), + PUTFIELD(Opcodes.PUTFIELD), + + INVOKEVIRTUAL(Opcodes.INVOKEVIRTUAL), + INVOKESPECIAL(Opcodes.INVOKESPECIAL), + INVOKESTATIC(Opcodes.INVOKESTATIC), + INVOKEINTERFACE(Opcodes.INVOKEINTERFACE), + INVOKEDYNAMIC(Opcodes.INVOKEDYNAMIC), + + NEW(Opcodes.NEW), + NEWARRAY(Opcodes.NEWARRAY), + ANEWARRAY(Opcodes.ANEWARRAY), + ARRAYLENGTH(Opcodes.ARRAYLENGTH), + ATHROW(Opcodes.ATHROW), + CHECKCAST(Opcodes.CHECKCAST), + INSTANCEOF(Opcodes.INSTANCEOF), + MONITORENTER(Opcodes.MONITORENTER), + MONITOREXIT(Opcodes.MONITOREXIT), + MULTIANEWARRAY(Opcodes.MULTIANEWARRAY), + IFNULL(Opcodes.IFNULL), + IFNONNULL(Opcodes.IFNONNULL); + + companion object { + + private val byOpcode: Map = values().associateBy { it.opcode } + + /** 按 ASM 指令值反查枚举;未知值返回 `null`,通常用于调试或诊断输出。 */ + @JvmStatic + fun fromInt(v: Int): Op? = byOpcode[v] + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Operation.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Operation.kt new file mode 100644 index 000000000..7d9644bda --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Operation.kt @@ -0,0 +1,35 @@ +package taboolib.module.incision.annotation + +/** + * 单条 advice 的运行元信息。 + * + * 用途: + * 在不改变 advice 主语义的前提下,补充执行优先级、默认启用状态和稳定 id。 + * + * 使用: + * 把它附加在任意 advice 方法上。若类级 [Surgeon] 已设置默认优先级, + * [priority] 会作为方法级覆盖值。 + * + * 效果: + * - [priority] 越大越先执行 + * - [enabled] 为 `false` 时默认不启用,需要后续手动 resume + * - [id] 用于稳定识别、排错和管理该 operation + * + * 局限: + * 1. 它只描述元信息,不改变 advice 本身的 handler 签名要求。 + * 2. 如果显式 [id] 管理不当,仍可能造成命名冲突或诊断困难。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Operation( + + /** 方法级优先级覆盖;数值越大越先执行。 */ + val priority: Int = 0, + + /** 是否默认启用;若为 `false`,后续需通过 `Suture.resume()` 手动启用。 */ + val enabled: Boolean = true, + + /** 显式 operation id 后缀;未指定时默认使用方法名。 */ + val id: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Scope.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Scope.kt new file mode 100644 index 000000000..010467043 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Scope.kt @@ -0,0 +1,30 @@ +package taboolib.module.incision.annotation + +/** + * 目标选择器注解。 + * + * 用途: + * 用一段 DSL 描述“这个 advice 到底针对哪些类、方法或字段”,避免把选择逻辑散落在代码里。 + * + * 使用: + * 可以标在 `@Surgeon` class 或具体 advice 方法上。 + * 常见语法: + * - `class:com.foo.Bar` + * - `method:Foo#bar(*)` + * - `field:Foo#name:String` + * - `&` 与,`|` 或,`!` 非 + * + * 效果: + * 运行时会先解析 [value],再用它筛掉不相关目标,从而减少无意义织入。 + * + * 局限: + * 1. 它负责“挑对象”,不负责“挑字节码里的具体位置”;后者应交给 [Site] 或 [InsnPattern]。 + * 2. 复杂表达式越多,可读性越差,维护时应优先保持 selector 简洁。 + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Scope( + /** 选择器 DSL 原文。 */ + val value: String, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Site.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Site.kt new file mode 100644 index 000000000..8820e7075 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Site.kt @@ -0,0 +1,37 @@ +package taboolib.module.incision.annotation + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.Shift + +/** + * 锚点描述。 + * + * 用途: + * 把“织入发生在方法里的哪里”表达成结构化参数,供 [Graft]、[Bypass]、[Trim] 等注解复用。 + * + * 使用: + * 通过 [anchor] 指定锚点种类,例如 `HEAD`、`INVOKE`、`FIELD_GET`; + * 再通过 [target]、[shift]、[ordinal]、[offset] 精确限定第几个命中、前后偏移多少条指令。 + * + * 效果: + * 运行时会先解析出候选锚点,再结合 ordinal、offset 等条件得到最终织入位置。 + * + * 局限: + * 1. 锚点最终还是依赖真实字节码,源码重构可能改变命中结果。 + * 2. `ordinal = -1` 表示全部命中,若目标方法里相同位点很多,执行次数会相应增加。 + * + * @property anchor 锚点类型,决定以哪类字节码事件作为定位基准。 + * @property target 锚点目标描述符;例如 `INVOKE` 时通常写成 `类#方法名(参数)返回`。 + * @property shift 相对锚点的方向,通常为 BEFORE 或 AFTER。 + * @property ordinal 第几个命中;`-1` 表示全部命中。 + * @property offset 沿 [shift] 方向再额外跨越的指令数;`0` 表示停在锚点本身。 + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Site( + val anchor: Anchor, + val target: String = "", + val shift: Shift = Shift.BEFORE, + val ordinal: Int = -1, + val offset: Int = 0, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Splice.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Splice.kt new file mode 100644 index 000000000..19c665a4a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Splice.kt @@ -0,0 +1,34 @@ +package taboolib.module.incision.annotation + +/** + * 环绕 advice。 + * + * 用途: + * 完整包裹原方法,适合做“先判断再决定是否放行”的逻辑,例如权限判断、参数改写、 + * 返回值接管、短路返回等。 + * + * 使用: + * handler 常见签名为 `fun(theatre: Theatre): Any?`。命中后必须显式调用 + * `theatre.resume.proceed()`、`theatre.resume.proceed(args...)`、 + * `theatre.resume.proceedResult(...)` 或 `theatre.override(...)` 之一。 + * + * 效果: + * `@Splice` 能同时观察调用前、调用中和调用后,是控制力最强的 advice 形式。 + * + * 局限: + * 1. 没有显式 resume/override 会触发 `Trauma.Runtime.ResumeMissing`。 + * 2. 控制面越大,越需要确保 handler 与目标返回类型、异常语义保持一致。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Splice( + /** 目标选择器 DSL。 */ + val scope: String, + + /** 额外的指令序列约束,用于限制只对满足特定字节码形态的目标方法生效。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Step.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Step.kt new file mode 100644 index 000000000..37dbbac0b --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Step.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.annotation + +/** + * 指令模式中的单个步骤。 + * + * 用途: + * 描述一条字节码指令需要满足什么条件,多个步骤串联后构成 [InsnPattern]。 + * + * 使用: + * 最少只需要提供 [opcode];当你要匹配调用、字段访问或常量加载时,再补充 + * [owner]、[name]、[desc]、[cst]、[repeat] 等条件。 + * + * 效果: + * 匹配器会按字段逐项过滤;空字符串代表“不约束”,[Op.ANY] 代表任意 opcode。 + * + * 局限: + * 1. `desc`、`owner` 等都基于 ASM 视角,不是 Kotlin/Java 源码写法。 + * 2. glob 适合做通配,但过度宽松时很容易把非预期指令也算进来。 + * + * @property opcode 期望的字节码指令;[Op.ANY] 表示任意指令。 + * @property owner 调用或字段访问所属类的 ASM internal name,支持 glob。 + * @property name 方法名或字段名,支持 glob。 + * @property desc 方法描述符或字段类型描述符,支持 glob。 + * @property cst 常量值匹配,常用于 LDC、BIPUSH、SIPUSH 等指令。 + * @property repeat 连续重复命中的次数;默认为 1。 + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Step( + val opcode: Op, + val owner: String = "", + val name: String = "", + val desc: String = "", + val cst: String = "", + val repeat: Int = 1 +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Surgeon.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Surgeon.kt new file mode 100644 index 000000000..5213784c0 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Surgeon.kt @@ -0,0 +1,28 @@ +package taboolib.module.incision.annotation + +/** + * 注解式施术者声明。 + * + * 用途: + * 把一个 Kotlin `object` 声明为 advice 持有者,让扫描器能在其中发现 `@Lead`、`@Trail` + * 等注解方法。 + * + * 使用: + * 只能标在 Kotlin `object` 上;标在普通 class 上会在启动检查阶段抛出 + * `Trauma.Declaration.InvalidHolder`。 + * + * 效果: + * 被标注的 object 会参与扫描,其内部 advice 方法会按照类级默认优先级注册。 + * + * 局限: + * 1. 该注解只负责“这个 object 要不要被扫描”,不负责具体 target 选择。 + * 2. 类级 [priority] 只是默认值,仍可能被方法级 [Operation] 覆盖。 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Surgeon( + + /** 该 object 内 advice 的默认优先级;数值越大越先执行。 */ + val priority: Int = 0, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/SurgeryDesk.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/SurgeryDesk.kt new file mode 100644 index 000000000..d8ae1d936 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/SurgeryDesk.kt @@ -0,0 +1,27 @@ +package taboolib.module.incision.annotation + +/** + * DSL 手术工作台声明。 + * + * 用途: + * 标记一个 Kotlin `object` 允许持有 `scalpel { ... }` 委托属性和 `scalpel.transient { ... }` + * 短期手术块。 + * + * 使用: + * 把它标在调用 DSL 的 object 上。运行时会基于调用栈检查“当前是否位于合法 desk 内”。 + * + * 效果: + * 合法 desk 内声明的 DSL 手术会继承该 desk 的默认优先级,并通过栈帧校验获得运行资格。 + * + * 局限: + * 1. 如果在未标注的 object/class 内调用 DSL,会在 `provideDelegate` 或运行时检查阶段失败。 + * 2. 它只约束 DSL 调用来源,不影响注解式 `@Surgeon` 的扫描逻辑。 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class SurgeryDesk( + + /** 该 desk 内 DSL 手术的默认优先级;数值越大越先执行。 */ + val priority: Int = 0, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trail.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trail.kt new file mode 100644 index 000000000..1f0a2727e --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trail.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.annotation + +/** + * 尾随 advice。 + * + * 用途: + * 在目标方法所有出口处执行收尾逻辑,适合做日志、计数、清理和返回值观察。 + * + * 使用: + * 把注解标在 `@Surgeon` object 的方法上,通过 [scope] 指定目标; + * 如需覆盖异常出口,保持 [onThrow] 为 `true`。 + * + * 效果: + * advice 会在 `return` 之前或异常即将抛出时触发,可通过 `Theatre.throwable` + * 区分正常返回与异常出口。 + * + * 局限: + * 1. 它关注的是“离开目标方法”的时刻,不适合表达方法中段的局部锚点。 + * 2. 如果关闭 [onThrow],异常出口不会执行该 advice。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Trail( + /** 目标选择器 DSL。 */ + val scope: String, + + /** 是否同时覆盖异常出口;为 `false` 时仅在正常返回时触发。 */ + val onThrow: Boolean = true, + + /** 额外的指令序列约束,用于限制只在特定字节码结构的目标方法上生效。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trim.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trim.kt new file mode 100644 index 000000000..f937a7015 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Trim.kt @@ -0,0 +1,47 @@ +package taboolib.module.incision.annotation + +import taboolib.module.incision.api.Anchor + +/** + * 值改写 advice。 + * + * 用途: + * 修改某个位置正在流动的值,包括方法参数、返回值和局部变量。 + * + * 使用: + * 通过 [method] 指定宿主方法,通过 [kind] 指定要改写的是参数、返回值还是局部变量; + * 当 [kind] 为参数或局部变量时,再用 [index] 指定具体槽位。 + * + * 效果: + * 命中后 handler 返回的新值会覆盖原值,从而改变后续执行结果。 + * + * 局限: + * 1. 需要保证返回值类型与被改写位点兼容。 + * 2. 局部变量场景依赖局部槽位布局,源码小改动就可能改变命中位置。 + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Trim( + /** 宿主方法描述符,格式为 `类#方法名(参数)返回`。 */ + val method: String, + + /** 改写目标类型:参数、返回值或局部变量。 */ + val kind: Kind, + + /** 参数索引(kind=ARG)或局部变量槽位(kind=VAR);返回值场景通常保持默认。 */ + val index: Int = 0, + + /** 改写落点;默认在方法入口处理。 */ + val site: Site = Site(Anchor.HEAD), + + /** 额外的指令序列约束,用于在同一宿主方法中进一步缩小命中范围。 */ + val pattern: InsnPattern = InsnPattern([]), + + /** 命名标签,可供 DSL、诊断输出或后续匹配事件引用。 */ + val where: String = "", +) { + + /** Trim 支持的值类别。 */ + enum class Kind { ARG, RETURN, VAR } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Version.kt b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Version.kt new file mode 100644 index 000000000..b708d8d29 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/annotation/Version.kt @@ -0,0 +1,38 @@ +package taboolib.module.incision.annotation + +/** + * 版本范围过滤注解。 + * + * 用途: + * 让同一份 advice 只在指定运行环境版本区间内注册,常用于 NMS、平台差异或兼容层切换。 + * + * 使用: + * 把它与任意 advice 注解同时标在同一方法上。扫描阶段会先用 [matcher] 解析当前版本, + * 再判断它是否落在 [`start`, `end`] 闭区间内。 + * + * 效果: + * - `start = ""` 表示无下界 + * - `end = ""` 表示无上界 + * - 两端都为空时,等价于始终生效 + * + * 比较规则: + * 版本字符串采用 Minecraft 风格 dot-decimal,例如 `1.20`、`1.20.4`、`1.21.1`, + * 按段数值比较,缺失段视为 0。 + * + * 局限: + * 1. 默认 matcher 读取的是 Minecraft/NMS 语义;若你的版本来源不同,应显式提供 [matcher]。 + * 2. `matcher` 解析失败时会回退到 Noop matcher,这通常意味着该 advice 不会按你预期筛选。 + * + * @see taboolib.module.incision.api.VersionMatcher + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Version( + /** 起始版本(含);空字符串表示无下界。 */ + val start: String = "", + /** 结束版本(含);空字符串表示无上界。 */ + val end: String = "", + /** 版本来源匹配器实现类的 FQCN;留空时使用默认的 Minecraft matcher。 */ + val matcher: String = "", +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Accessors.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Accessors.kt new file mode 100644 index 000000000..b44aaac14 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Accessors.kt @@ -0,0 +1,159 @@ +package taboolib.module.incision.api + +import taboolib.module.incision.diagnostic.Trauma + +/** + * Lambda 工厂 — handler 字段/方法访问的主推 API。 + * + * 在类级别声明 accessor,handler 内直接 invoke: + * ```kotlin + * private val teleportOwner = field("teleportOwner") + * private val moveConstant = staticField(AsyncTimedTeleport::class.java, "MOVE_CONSTANT") + * private val setRespawn = fieldSet(AsyncTimedTeleport::class.java, "timer_respawn") + * private val doCheck = method("checkPermission") + * + * @Surgeon(...) + * fun cooldown(t: Theatre) { + * val owner = teleportOwner(t) + * val limit = moveConstant() + * setRespawn(t, true) + * val ok = doCheck(t, "essentials.teleport") + * } + * ``` + * + * 每个 accessor 在首次调用时解析字段/方法并缓存,后续调用只做一次 volatile read + JNI/反射 dispatch。 + * + * **注意**:修改 `static final` 原始类型或 String 字段可能不会对已 JIT 过的调用点生效(常量折叠)。 + * 实例 final 字段不受影响。 + */ + +// ============================================================ +// 顶层工厂函数 +// ============================================================ + +/** 读 self 上的实例字段(ownerClass 从 self.javaClass 继承链自动解析) */ +fun field(name: String): FieldAccessor = FieldAccessor(null, name) + +/** 读实例字段,指定声明类(解决 private 字段在父类的场景) */ +fun field(ownerClass: Class<*>, name: String): FieldAccessor = FieldAccessor(ownerClass, name) + +/** 读 static 字段 */ +fun staticField(ownerClass: Class<*>, name: String): StaticFieldAccessor = StaticFieldAccessor(ownerClass, name) + +/** 写 self 上的实例字段 */ +fun fieldSet(name: String): FieldSetter = FieldSetter(null, name) + +/** 写实例字段,指定声明类 */ +fun fieldSet(ownerClass: Class<*>, name: String): FieldSetter = FieldSetter(ownerClass, name) + +/** 写 static 字段 */ +fun staticFieldSet(ownerClass: Class<*>, name: String): StaticFieldSetter = StaticFieldSetter(ownerClass, name) + +/** 调用 self 上的实例方法(descriptor 可选,省略则按 args 类型匹配) */ +fun method(name: String, descriptor: String? = null): MethodAccessor = MethodAccessor(null, name, descriptor) + +/** 调用实例方法,指定声明类 */ +fun method(ownerClass: Class<*>, name: String, descriptor: String? = null): MethodAccessor = MethodAccessor(ownerClass, name, descriptor) + +/** 调用 static 方法 */ +fun staticMethod(ownerClass: Class<*>, name: String, descriptor: String? = null): StaticMethodAccessor = StaticMethodAccessor(ownerClass, name, descriptor) + +// ============================================================ +// Accessor 类族 +// ============================================================ + +class FieldAccessor(private val ownerClass: Class<*>?, private val name: String) { + + @Volatile private var resolved: IncisionAccessor.ResolvedField? = null + + @Suppress("UNCHECKED_CAST") + operator fun invoke(theatre: Theatre): T? { + val self = theatre.self ?: throw Trauma.Accessor.StaticOnInstance(name) + val r = resolved ?: IncisionAccessor.resolveField(ownerClass ?: self.javaClass, name).also { resolved = it } + return IncisionAccessor.fieldGet(self, r) + } + + @Suppress("UNCHECKED_CAST") + operator fun invoke(receiver: Any): T? { + val r = resolved ?: IncisionAccessor.resolveField(ownerClass ?: receiver.javaClass, name).also { resolved = it } + return IncisionAccessor.fieldGet(receiver, r) + } +} + +class FieldSetter(private val ownerClass: Class<*>?, private val name: String) { + + @Volatile private var resolved: IncisionAccessor.ResolvedField? = null + + operator fun invoke(theatre: Theatre, value: T?) { + val self = theatre.self ?: throw Trauma.Accessor.StaticOnInstance(name) + val r = resolved ?: IncisionAccessor.resolveField(ownerClass ?: self.javaClass, name).also { resolved = it } + IncisionAccessor.fieldSet(self, r, value) + } + + operator fun invoke(receiver: Any, value: T?) { + val r = resolved ?: IncisionAccessor.resolveField(ownerClass ?: receiver.javaClass, name).also { resolved = it } + IncisionAccessor.fieldSet(receiver, r, value) + } +} + +class StaticFieldAccessor(private val ownerClass: Class<*>, private val name: String) { + + @Volatile private var resolved: IncisionAccessor.ResolvedField? = null + + @Suppress("UNCHECKED_CAST") + operator fun invoke(): T? { + val r = resolved ?: IncisionAccessor.resolveField(ownerClass, name).also { resolved = it } + return IncisionAccessor.staticFieldGet(r) + } + + operator fun invoke(@Suppress("UNUSED_PARAMETER") theatre: Theatre): T? = invoke() +} + +class StaticFieldSetter(private val ownerClass: Class<*>, private val name: String) { + + @Volatile private var resolved: IncisionAccessor.ResolvedField? = null + + operator fun invoke(value: T?) { + val r = resolved ?: IncisionAccessor.resolveField(ownerClass, name).also { resolved = it } + IncisionAccessor.staticFieldSet(r, value) + } + + operator fun invoke(@Suppress("UNUSED_PARAMETER") theatre: Theatre, value: T?) = invoke(value) +} + +class MethodAccessor(private val ownerClass: Class<*>?, private val name: String, private val descriptor: String?) { + + @Volatile private var resolved: IncisionAccessor.ResolvedMethod? = null + + @Suppress("UNCHECKED_CAST") + operator fun invoke(theatre: Theatre, vararg args: Any?): T? { + val self = theatre.self ?: throw Trauma.Accessor.StaticOnInstance(name) + val a = arrayOf(*args) + val r = resolved + ?: IncisionAccessor.resolveMethod(ownerClass ?: self.javaClass, name, descriptor, a).also { resolved = it } + return IncisionAccessor.invokeResolved(self, r, a) + } + + @Suppress("UNCHECKED_CAST") + operator fun invoke(receiver: Any, vararg args: Any?): T? { + val a = arrayOf(*args) + val r = resolved + ?: IncisionAccessor.resolveMethod(ownerClass ?: receiver.javaClass, name, descriptor, a).also { resolved = it } + return IncisionAccessor.invokeResolved(receiver, r, a) + } +} + +class StaticMethodAccessor(private val ownerClass: Class<*>, private val name: String, private val descriptor: String?) { + + @Volatile private var resolved: IncisionAccessor.ResolvedMethod? = null + + @Suppress("UNCHECKED_CAST") + operator fun invoke(vararg args: Any?): T? { + val a = arrayOf(*args) + val r = resolved + ?: IncisionAccessor.resolveMethod(ownerClass, name, descriptor, a).also { resolved = it } + return IncisionAccessor.invokeResolved(null, r, a) + } + + operator fun invoke(@Suppress("UNUSED_PARAMETER") theatre: Theatre, vararg args: Any?): T? = invoke(*args) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Anatomy.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Anatomy.kt new file mode 100644 index 000000000..351f035c1 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Anatomy.kt @@ -0,0 +1,15 @@ +package taboolib.module.incision.api + +/** + * 目标解析快照 — 在 Checkup 阶段产生,记录扫描期对一个切术的解析结果。 + */ +data class Anatomy( + val incisionId: String, + val rawDescriptor: String, + val translatedDescriptor: String?, + val coordinate: MethodCoordinate?, + val resolverName: String, + val resolverEnv: String, + val ok: Boolean, + val reason: String? = null, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Anchor.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Anchor.kt new file mode 100644 index 000000000..ba5fbd2ca --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Anchor.kt @@ -0,0 +1,30 @@ +package taboolib.module.incision.api + +/** + * 锚点类型 — 描述在目标方法中的插入位置。 + */ +enum class Anchor { + /** 方法入口 */ + HEAD, + + /** 方法所有正常出口(每个 return 前) */ + TAIL, + + /** 方法的 return 指令(可含返回值修改) */ + RETURN, + + /** 方法调用指令处(INVOKEVIRTUAL / INVOKESTATIC / INVOKESPECIAL / INVOKEINTERFACE) */ + INVOKE, + + /** 字段读取 (GETFIELD / GETSTATIC) */ + FIELD_GET, + + /** 字段写入 (PUTFIELD / PUTSTATIC) */ + FIELD_PUT, + + /** new 指令(对象构造) */ + NEW, + + /** throw 指令 */ + THROW, +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/DescriptorCodec.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/DescriptorCodec.kt new file mode 100644 index 000000000..79082b2e1 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/DescriptorCodec.kt @@ -0,0 +1,78 @@ +package taboolib.module.incision.api + +/** + * 描述符工具 — 把用户写的宽松描述符规范为 JVM 描述符。 + * + * 支持: + * - 完整 `类#方法名(Ljava/lang/String;)V` + * - 通配实参 `类#方法名(*)` → 后期 scope 匹配 + * - `{version}` 占位符(需与 NMS resolver 协同) + * - 短描述符 `String, int` → `Ljava/lang/String;I` + */ +object DescriptorCodec { + + private val primitives = mapOf( + "void" to "V", "boolean" to "Z", "byte" to "B", "short" to "S", + "int" to "I", "long" to "J", "float" to "F", "double" to "D", "char" to "C", + ) + + /** + * 把 `类#方法名(参数)返回` 拆成 owner / name / 方法描述符。 + * 返回 null 表示解析失败(不抛异常,交给上游决定处理方式)。 + */ + fun parseMethod(raw: String): Parsed? { + val hash = raw.indexOf('#') + if (hash <= 0 || hash == raw.length - 1) return null + val owner = raw.substring(0, hash).trim().takeIf { it.isNotEmpty() }?.replace('.', '/') ?: return null + val rest = raw.substring(hash + 1).trim() + if (rest.isEmpty()) return null + val paren = rest.indexOf('(') + if (paren <= 0) return null + val close = rest.indexOf(')', paren) + if (close < 0) return null + val name = rest.substring(0, paren) + if (name.isBlank() || name.any(Char::isWhitespace)) return null + val argsPart = rest.substring(paren + 1, close) + val retPart = rest.substring(close + 1).ifBlank { if (argsPart == "*") "*" else "V" } + val argsDesc = encodeArgs(argsPart) + val retDesc = encodeType(retPart) + return Parsed(owner, name, "($argsDesc)$retDesc") + } + + fun parseField(raw: String): ParsedField? { + val hash = raw.indexOf('#') + if (hash < 0) return null + val owner = raw.substring(0, hash).replace('.', '/') + val rest = raw.substring(hash + 1) + val colon = rest.indexOf(':') + if (colon < 0) return null + val name = rest.substring(0, colon) + val type = rest.substring(colon + 1) + return ParsedField(owner, name, encodeType(type)) + } + + private fun encodeArgs(args: String): String { + if (args.isBlank() || args == "*") return args + return args.split(',').filter { it.isNotBlank() }.joinToString("") { encodeType(it.trim()) } + } + + private val jvmPrimitiveDescriptors = setOf("V", "Z", "B", "S", "I", "J", "F", "D", "C") + + fun encodeType(raw: String): String { + if (raw == "*") return "*" + var arr = 0 + var t = raw + while (t.endsWith("[]")) { arr++; t = t.dropLast(2) } + val base = primitives[t] ?: when { + t.length == 1 && t in jvmPrimitiveDescriptors -> t + t.startsWith("L") && t.endsWith(";") -> t + else -> "L${t.replace('.', '/')};" + } + return "[".repeat(arr) + base + } + + data class Parsed(val owner: String, val name: String, val descriptor: String) { + fun toCoordinate() = MethodCoordinate(owner, name, descriptor) + } + data class ParsedField(val owner: String, val name: String, val descriptor: String) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/IncisionAccessor.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/IncisionAccessor.kt new file mode 100644 index 000000000..e4163c403 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/IncisionAccessor.kt @@ -0,0 +1,311 @@ +package taboolib.module.incision.api + +import org.objectweb.asm.Type +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.JvmtiBackend +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.concurrent.ConcurrentHashMap + +/** + * 字段/方法访问的底层门面。 + * + * 所有 handler 侧的字段读写、方法调用最终都走这里。调度优先级: + * 1. JvmtiBackend(JNI,无视 Java 访问控制与 final) + * 2. 反射 `setAccessible(true)`(JDK 17+ 非开放模块可能拒绝) + * 3. `sun.misc.Unsafe`(仅对 final 字段写入使用的兜底) + * + * 解析结果缓存在 [fieldCache] / [methodCache],避免热路径上反复反射查表。 + */ +object IncisionAccessor { + + // ---- 对外 API ---- + + fun fieldGet(receiver: Any, ownerClass: Class<*>, name: String): T? { + val r = resolveField(ownerClass, name) + return fieldGet(receiver, r) + } + + fun fieldSet(receiver: Any, ownerClass: Class<*>, name: String, value: Any?) { + val r = resolveField(ownerClass, name) + fieldSet(receiver, r, value) + } + + fun staticFieldGet(ownerClass: Class<*>, name: String): T? { + val r = resolveField(ownerClass, name) + return staticFieldGet(r) + } + + fun staticFieldSet(ownerClass: Class<*>, name: String, value: Any?) { + val r = resolveField(ownerClass, name) + staticFieldSet(r, value) + } + + fun invoke(receiver: Any?, ownerClass: Class<*>, name: String, descriptor: String?, args: Array): T? { + val r = resolveMethod(ownerClass, name, descriptor, args) + return invokeResolved(receiver, r, args) + } + + // ---- 解析层(供 Accessors.kt 预热) ---- + + @Suppress("UNCHECKED_CAST") + fun fieldGet(receiver: Any, resolved: ResolvedField): T? { + if (JvmtiBackend.available()) { + return JvmtiBackend.fieldGet(receiver, resolved.declaringClass, resolved.field.name, resolved.descriptor) as T? + } + return try { + resolved.field.get(receiver) as T? + } catch (t: Throwable) { + throw denied(resolved.declaringClass, resolved.field.name, "反射读取失败", t) + } + } + + fun fieldSet(receiver: Any, resolved: ResolvedField, value: Any?) { + if (JvmtiBackend.available()) { + JvmtiBackend.fieldSet(receiver, resolved.declaringClass, resolved.field.name, resolved.descriptor, value) + return + } + val f = resolved.field + try { + f.set(receiver, value) + return + } catch (t: Throwable) { + if (Modifier.isFinal(f.modifiers)) { + // final 字段,反射通常被拒 — 走 Unsafe + if (UnsafeBridge.setInstanceFinal(receiver, f, value)) return + } + throw denied(resolved.declaringClass, f.name, "反射写入失败 (final=${Modifier.isFinal(f.modifiers)})", t) + } + } + + @Suppress("UNCHECKED_CAST") + fun staticFieldGet(resolved: ResolvedField): T? { + if (JvmtiBackend.available()) { + return JvmtiBackend.staticFieldGet(resolved.declaringClass, resolved.field.name, resolved.descriptor) as T? + } + return try { + resolved.field.get(null) as T? + } catch (t: Throwable) { + throw denied(resolved.declaringClass, resolved.field.name, "反射静态读取失败", t) + } + } + + fun staticFieldSet(resolved: ResolvedField, value: Any?) { + if (JvmtiBackend.available()) { + JvmtiBackend.staticFieldSet(resolved.declaringClass, resolved.field.name, resolved.descriptor, value) + return + } + val f = resolved.field + try { + f.set(null, value) + return + } catch (t: Throwable) { + if (Modifier.isFinal(f.modifiers)) { + if (UnsafeBridge.setStaticFinal(f, value)) return + } + throw denied(resolved.declaringClass, f.name, "反射静态写入失败 (final=${Modifier.isFinal(f.modifiers)})", t) + } + } + + @Suppress("UNCHECKED_CAST") + fun invokeResolved(receiver: Any?, resolved: ResolvedMethod, args: Array): T? { + if (JvmtiBackend.available()) { + return JvmtiBackend.invokeMethod( + receiver, + resolved.declaringClass, + resolved.method.name, + resolved.descriptor, + args as Array?, + ) as T? + } + val m = resolved.method + return try { + m.invoke(receiver, *args) as T? + } catch (t: java.lang.reflect.InvocationTargetException) { + throw t.cause ?: t + } catch (t: Throwable) { + throw denied(resolved.declaringClass, m.name, "反射调用失败", t) + } + } + + // ---- 缓存解析 ---- + + fun resolveField(ownerClass: Class<*>, name: String): ResolvedField { + val key = FieldKey(ownerClass, name) + fieldCache[key]?.let { return it } + var c: Class<*>? = ownerClass + while (c != null) { + val f = try { c.getDeclaredField(name) } catch (_: NoSuchFieldException) { null } + if (f != null) { + runCatching { f.isAccessible = true } + val desc = Type.getDescriptor(f.type) + val r = ResolvedField(f, c, desc) + fieldCache.putIfAbsent(key, r) + return r + } + c = c.superclass + } + throw Trauma.Accessor.FieldNotFound(ownerClass.name, name) + } + + fun resolveMethod(ownerClass: Class<*>, name: String, descriptor: String?, args: Array): ResolvedMethod { + val key = MethodKey(ownerClass, name, descriptor ?: argsSignature(args)) + methodCache[key]?.let { return it } + val candidates = collectMethods(ownerClass, name) + if (candidates.isEmpty()) { + throw Trauma.Accessor.MethodNotFound(ownerClass.name, name, descriptor, emptyList()) + } + val picked = if (descriptor != null) { + candidates.firstOrNull { Type.getMethodDescriptor(it) == descriptor } + ?: throw Trauma.Accessor.MethodNotFound( + ownerClass.name, name, descriptor, + candidates.map { Type.getMethodDescriptor(it) }, + ) + } else { + val matched = candidates.filter { isArgCompatible(it, args) } + when (matched.size) { + 0 -> throw Trauma.Accessor.MethodNotFound( + ownerClass.name, name, null, + candidates.map { Type.getMethodDescriptor(it) }, + ) + 1 -> matched.single() + else -> throw Trauma.Accessor.AmbiguousMethod( + ownerClass.name, name, matched.map { Type.getMethodDescriptor(it) }, + ) + } + } + runCatching { picked.isAccessible = true } + val r = ResolvedMethod(picked, picked.declaringClass, Type.getMethodDescriptor(picked)) + methodCache.putIfAbsent(key, r) + return r + } + + // ---- 内部辅助 ---- + + private fun collectMethods(ownerClass: Class<*>, name: String): List { + val out = ArrayList() + var c: Class<*>? = ownerClass + while (c != null) { + c.declaredMethods.filterTo(out) { it.name == name } + c = c.superclass + } + return out + } + + private fun isArgCompatible(m: Method, args: Array): Boolean { + val p = m.parameterTypes + if (p.size != args.size) return false + for (i in p.indices) { + val a = args[i] ?: continue + if (!wrap(p[i]).isAssignableFrom(a.javaClass)) return false + } + return true + } + + private fun wrap(c: Class<*>): Class<*> = when (c) { + java.lang.Integer.TYPE -> java.lang.Integer::class.java + java.lang.Long.TYPE -> java.lang.Long::class.java + java.lang.Short.TYPE -> java.lang.Short::class.java + java.lang.Byte.TYPE -> java.lang.Byte::class.java + java.lang.Boolean.TYPE -> java.lang.Boolean::class.java + java.lang.Character.TYPE -> java.lang.Character::class.java + java.lang.Float.TYPE -> java.lang.Float::class.java + java.lang.Double.TYPE -> java.lang.Double::class.java + else -> c + } + + private fun argsSignature(args: Array): String = buildString { + append('#') + args.forEach { a -> append(a?.javaClass?.name ?: "*"); append(';') } + } + + private fun denied(ownerClass: Class<*>, member: String, reason: String, cause: Throwable): Trauma.Accessor.AccessDenied { + val t = Trauma.Accessor.AccessDenied(ownerClass.name, member, reason, cause) + Forensics.debug("Accessor 降级失败: ${ownerClass.name}.$member — $reason (${cause.javaClass.simpleName}: ${cause.message})") + return t + } + + // ---- 数据类型 ---- + + data class FieldKey(val ownerClass: Class<*>, val name: String) + data class MethodKey(val ownerClass: Class<*>, val name: String, val descriptorOrArgSig: String) + + class ResolvedField(val field: Field, val declaringClass: Class<*>, val descriptor: String) + class ResolvedMethod(val method: Method, val declaringClass: Class<*>, val descriptor: String) + + private val fieldCache = ConcurrentHashMap() + private val methodCache = ConcurrentHashMap() +} + +/** + * Unsafe 兜底 — 仅当 JVMTI 不可用且反射写入 final 字段被拒时启用。 + * + * 实例字段覆盖所有基元 + 对象类型;静态同理。任何一步失败直接返回 false,由上游抛 AccessDenied。 + */ +private object UnsafeBridge { + + private val unsafe: Any? by lazy { + try { + val c = Class.forName("sun.misc.Unsafe") + val f = c.getDeclaredField("theUnsafe") + f.isAccessible = true + f.get(null) + } catch (_: Throwable) { null } + } + + private val objectFieldOffset = method("objectFieldOffset", java.lang.reflect.Field::class.java) + private val staticFieldBase = method("staticFieldBase", java.lang.reflect.Field::class.java) + private val staticFieldOffset = method("staticFieldOffset", java.lang.reflect.Field::class.java) + + fun setInstanceFinal(receiver: Any, field: Field, value: Any?): Boolean { + val u = unsafe ?: return false + val off = try { objectFieldOffset!!.invoke(u, field) as Long } catch (_: Throwable) { return false } + return putByType(u, receiver, off, field.type, value, false) + } + + fun setStaticFinal(field: Field, value: Any?): Boolean { + val u = unsafe ?: return false + val base = try { staticFieldBase!!.invoke(u, field) } catch (_: Throwable) { return false } + val off = try { staticFieldOffset!!.invoke(u, field) as Long } catch (_: Throwable) { return false } + return putByType(u, base, off, field.type, value, true) + } + + private fun putByType(u: Any, target: Any, off: Long, type: Class<*>, value: Any?, @Suppress("UNUSED_PARAMETER") isStatic: Boolean): Boolean { + return try { + when (type) { + java.lang.Integer.TYPE -> call(u, "putInt", target, off, value as Int) + java.lang.Long.TYPE -> call(u, "putLong", target, off, value as Long) + java.lang.Short.TYPE -> call(u, "putShort", target, off, value as Short) + java.lang.Byte.TYPE -> call(u, "putByte", target, off, value as Byte) + java.lang.Boolean.TYPE -> call(u, "putBoolean", target, off, value as Boolean) + java.lang.Character.TYPE -> call(u, "putChar", target, off, value as Char) + java.lang.Float.TYPE -> call(u, "putFloat", target, off, value as Float) + java.lang.Double.TYPE -> call(u, "putDouble", target, off, value as Double) + else -> call(u, "putObject", target, off, value) + } + true + } catch (_: Throwable) { false } + } + + private fun method(name: String, vararg params: Class<*>): java.lang.reflect.Method? = try { + unsafe?.javaClass?.getMethod(name, *params) + } catch (_: Throwable) { null } + + private fun call(u: Any, name: String, target: Any, off: Long, value: Any?) { + val cls = u.javaClass + val m = when (name) { + "putInt" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Integer.TYPE) + "putLong" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Long.TYPE) + "putShort" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Short.TYPE) + "putByte" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Byte.TYPE) + "putBoolean" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Boolean.TYPE) + "putChar" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Character.TYPE) + "putFloat" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Float.TYPE) + "putDouble" -> cls.getMethod(name, Any::class.java, java.lang.Long.TYPE, java.lang.Double.TYPE) + else -> cls.getMethod("putObject", Any::class.java, java.lang.Long.TYPE, Any::class.java) + } + m.invoke(u, target, off, value) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/MethodCoordinate.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/MethodCoordinate.kt new file mode 100644 index 000000000..6ec28209a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/MethodCoordinate.kt @@ -0,0 +1,21 @@ +package taboolib.module.incision.api + +/** + * 方法坐标 — 唯一标识 JVM 中的一个方法。 + * + * @property owner 类内部名(斜杠形式),如 `org/bukkit/entity/Player` + * @property name 方法名 + * @property descriptor 方法描述符,如 `(Ljava/lang/String;)V` + */ +data class MethodCoordinate( + val owner: String, + val name: String, + val descriptor: String, +) { + + val ownerClassName: String get() = owner.replace('/', '.') + + val signature: String get() = "$owner.$name$descriptor" + + override fun toString(): String = signature +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Resume.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Resume.kt new file mode 100644 index 000000000..b099d2168 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Resume.kt @@ -0,0 +1,26 @@ +package taboolib.module.incision.api + +/** + * 放行原指令的句柄,由 Splice / Around 类切术使用。 + * + * 调用 [proceed] 会按链式顺序触发下一条 advice,最终触发原方法。 + * 调用 [proceedResult] 可把一个结果继续传给下游 advice,而不立刻终止整条链。 + * 调用 [skip] 跳过下游 advice 直接终止,并使用给定值作为最终返回。 + */ +interface Resume { + + /** 放行至下游 advice 或原方法,返回链式调用结果 */ + fun proceed(): Any? + + /** 放行并替换实参 */ + fun proceed(vararg args: Any?): Any? + + /** 使用新的当前结果继续传给下游 advice */ + fun proceedResult(returnValue: Any?): Any? + + /** 跳过原方法,使用给定返回值终止链 */ + fun skip(returnValue: Any?): Any? + + /** 是否已显式放行过 */ + val proceeded: Boolean +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Shift.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Shift.kt new file mode 100644 index 000000000..008da588c --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Shift.kt @@ -0,0 +1,12 @@ +package taboolib.module.incision.api + +/** + * 相对锚点的偏移方向。 + */ +enum class Shift { + /** 在锚点指令之前插入 */ + BEFORE, + + /** 在锚点指令之后插入 */ + AFTER, +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Suture.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Suture.kt new file mode 100644 index 000000000..2a9d3485d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Suture.kt @@ -0,0 +1,66 @@ +package taboolib.module.incision.api + +import kotlin.reflect.KClass + +/** + * 织入产物句柄 — 持久切术的运行时引用。 + * + * 通过 [heal] 取消织入;通过 [suspend] / [resume] 临时启停而不卸下字节码。 + */ +interface Suture : AutoCloseable { + + /** 唯一识别名,格式 `FQCN#propertyName` */ + val id: String + + /** 此切术覆盖的目标方法集合 */ + val targets: List + + /** 当前状态 */ + val state: State + + /** 声明所在的 SurgeryDesk object */ + val holder: KClass<*> + + /** 永久卸载该 advice(可能触发字节码回滚) */ + fun heal(): Boolean + + /** 临时挂起 — 字节码保留,dispatcher 跳过 handler */ + fun suspend(): Boolean + + /** 恢复挂起的切术 */ + fun resume(): Boolean + + override fun close() { + heal() + } + + enum class State { + /** 已织入并启用 */ + ARMED, + + /** 已被触发过(统计用) */ + TRIGGERED, + + /** 已挂起,handler 跳过 */ + SUSPENDED, + + /** 已恢复,字节码已回滚或仅剩占位 */ + HEALED, + + /** 解析失败,运行期访问会再次抛 Trauma */ + INACTIVE_UNRESOLVED, + } +} + +/** + * 由 `on(...)` 等批量 DSL 创建的复合句柄。 + */ +interface BatchSuture : Suture { + + val children: List + + operator fun get(childId: String): Suture? + + /** 按谓词卸载子切术,返回卸载数量 */ + fun heal(predicate: (Suture) -> Boolean): Int +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/Theatre.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/Theatre.kt new file mode 100644 index 000000000..1a243004a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/Theatre.kt @@ -0,0 +1,139 @@ +package taboolib.module.incision.api + +import taboolib.module.incision.diagnostic.Trauma + +/** + * 手术现场上下文 — 暴露给 advice handler 的唯一入口。 + * + * 涵盖目标方法的实参、自身实例、目标方法坐标,以及对原指令的放行控制。 + * + * ## 字段与方法访问 + * + * 推荐使用类级 Lambda 工厂([field] / [staticField] / [method] 等顶层函数), + * 解析一次、多次复用、泛型参数化、JIT 友好。 + * + * 此接口上的 `field` / `staticField` / `setField` / `invoke` 等 default 方法 + * 适用于一次性、不值得声明 val 的场景。 + * + * **注意**:修改 `static final` 原始类型或 String 字段可能不会对已 JIT 过的调用点生效(常量折叠)。 + * 实例 final 字段不受影响。 + */ +interface Theatre { + + /** 目标方法坐标 */ + val target: MethodCoordinate + + /** 调用方实例(静态方法时为 null) */ + val self: Any? + + /** 实参数组(可读写;直接修改会反映到放行的调用) */ + val args: Array + + /** Resume 句柄;@Lead/@Trail 中调用会被忽略并标记为非法 */ + val resume: Resume + + /** + * 强制覆盖返回值并终止后续 advice 与原方法。 + * 等价于 `resume.skip(value)`。 + */ + fun override(value: Any?): Any? = resume.skip(value) + + /** 当前是否处于异常出口(@Trail 中可读) */ + val throwable: Throwable? + + // ---- 字段读取 ---- + + /** 读 self 上的实例字段 */ + fun field(name: String): T? { + val s = self ?: throw Trauma.Accessor.StaticOnInstance(name) + return IncisionAccessor.fieldGet(s, s.javaClass, name) + } + + /** 读 self 上的实例字段,指定声明类 */ + fun field(ownerClass: Class<*>, name: String): T? { + val s = self ?: throw Trauma.Accessor.StaticOnInstance(name) + return IncisionAccessor.fieldGet(s, ownerClass, name) + } + + /** 读任意实例的字段 */ + fun field(receiver: Any, ownerClass: Class<*>, name: String): T? = + IncisionAccessor.fieldGet(receiver, ownerClass, name) + + /** 读 static 字段 */ + fun staticField(ownerClass: Class<*>, name: String): T? = + IncisionAccessor.staticFieldGet(ownerClass, name) + + // ---- 字段写入 ---- + + /** 写 self 上的实例字段 */ + fun setField(name: String, value: Any?) { + val s = self ?: throw Trauma.Accessor.StaticOnInstance(name) + IncisionAccessor.fieldSet(s, s.javaClass, name, value) + } + + /** 写 self 上的实例字段,指定声明类 */ + fun setField(ownerClass: Class<*>, name: String, value: Any?) { + val s = self ?: throw Trauma.Accessor.StaticOnInstance(name) + IncisionAccessor.fieldSet(s, ownerClass, name, value) + } + + /** 写任意实例的字段 */ + fun setField(receiver: Any, ownerClass: Class<*>, name: String, value: Any?) = + IncisionAccessor.fieldSet(receiver, ownerClass, name, value) + + /** 写 static 字段 */ + fun setStaticField(ownerClass: Class<*>, name: String, value: Any?) = + IncisionAccessor.staticFieldSet(ownerClass, name, value) + + // ---- 方法调用 ---- + + /** 调用 self 上的实例方法 */ + fun invoke(name: String, vararg args: Any?): T? { + val s = self ?: throw Trauma.Accessor.StaticOnInstance(name) + return IncisionAccessor.invoke(s, s.javaClass, name, null, arrayOf(*args)) + } + + /** 调用任意实例的方法 */ + fun invoke(receiver: Any, ownerClass: Class<*>, name: String, descriptor: String? = null, vararg args: Any?): T? = + IncisionAccessor.invoke(receiver, ownerClass, name, descriptor, arrayOf(*args)) + + /** 调用 static 方法 */ + fun invokeStatic(ownerClass: Class<*>, name: String, vararg args: Any?): T? = + IncisionAccessor.invoke(null, ownerClass, name, null, arrayOf(*args)) + + // ---- 参数便捷访问 ---- + + /** 取第 [index] 个参数并强转为 [T];越界或类型不匹配返回 null */ + @Suppress("UNCHECKED_CAST") + fun arg(index: Int): T? = args.getOrNull(index) as? T + + /** 取第 [index] 个参数并强转为 [T];失败抛 ClassCastException / IndexOutOfBoundsException */ + @Suppress("UNCHECKED_CAST") + fun argOrThrow(index: Int): T = args[index] as T + + /** 安全转型:self 转为 [T],失败返回 null */ + @Suppress("UNCHECKED_CAST") + fun selfAs(): T? = self as? T +} + +// ============================================================ +// 顶层扩展 — 任何地方都能用,不受 Theatre receiver 作用域限制 +// ============================================================ + +/** 安全转型:失败返回 null */ +inline fun Any?.cast(): T? = this as? T + +/** 强制转型:失败抛 ClassCastException */ +inline fun Any?.castOrThrow(): T = this as T + +/** 在任意对象上读字段(自动沿继承链解析声明类,绕过访问控制) */ +fun Any.readField(name: String): T? = + IncisionAccessor.fieldGet(this, this.javaClass, name) + +/** 在任意对象上写字段(自动沿继承链解析声明类,绕过访问控制) */ +fun Any.writeField(name: String, value: Any?) = + IncisionAccessor.fieldSet(this, this.javaClass, name, value) + +/** 在任意对象上调用方法(自动沿继承链解析,按参数类型匹配,绕过访问控制) */ +fun Any.callMethod(name: String, vararg args: Any?): T? = + IncisionAccessor.invoke(this, this.javaClass, name, null, arrayOf(*args)) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/api/VersionMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/api/VersionMatcher.kt new file mode 100644 index 000000000..ed9edf0d6 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/api/VersionMatcher.kt @@ -0,0 +1,158 @@ +package taboolib.module.incision.api + +import taboolib.module.incision.diagnostic.Forensics + +/** + * 版本匹配器 —— 抽象"如何取当前版本"以及"如何判断版本是否落在区间内"。 + * + * 默认实现 [MinecraftVersionMatcher] 通过反射读取 `taboolib.module.nms.MinecraftVersion.runningVersion`, + * 当 bukkit-nms 模块不在 classpath 时回退为 [NoopVersionMatcher](始终命中)。 + * + * 外部插件可实现该接口,从自己的 manifest / 配置 / API 拿到版本字符串, + * 然后在 `@Version(matcher = "com.foo.MyMatcher")` 中指定 FQCN 接入。 + */ +interface VersionMatcher { + + /** + * 取当前运行环境下的版本字符串,例如 "1.20.4"。 + * 返回 null 表示无法获取 —— 此时 [matches] 应直接返回 true(不过滤)。 + */ + fun current(): String? + + /** + * 判断 [current] 是否落在 [start, end] 闭区间。空字符串端点视为无界。 + * 默认实现:dot-decimal 段比较;缺失段视为 0。 + */ + fun matches(start: String, end: String): Boolean { + val cur = current() ?: return true + if (start.isNotBlank() && compare(cur, start) < 0) return false + if (end.isNotBlank() && compare(cur, end) > 0) return false + return true + } + + /** dot-decimal 段比较:1.20 < 1.20.1 < 1.21;非数字段按字典序比较。 */ + fun compare(a: String, b: String): Int { + val sa = splitSegments(a) + val sb = splitSegments(b) + val n = maxOf(sa.size, sb.size) + for (i in 0 until n) { + val x = sa.getOrNull(i) ?: 0 + val y = sb.getOrNull(i) ?: 0 + if (x != y) return x.compareTo(y) + } + return 0 + } + + private fun splitSegments(s: String): List { + // 兼容形如 "1.20.4-SNAPSHOT" 的非纯数字尾巴:忽略尾巴、按数字段比较 + return s.split('.', '-', '_', '+').mapNotNull { it.toIntOrNull() } + } +} + +/** + * 永远命中的版本匹配器 —— 不过滤任何 advice。 + * + * 当 [MinecraftVersionMatcher] 拿不到 NMS 类时回退到这里,保证既有测试不被破坏。 + */ +object NoopVersionMatcher : VersionMatcher { + override fun current(): String? = null + override fun matches(start: String, end: String): Boolean = true +} + +/** + * 默认匹配器 —— 反射读取 [taboolib.module.nms.MinecraftVersion]。 + * + * 反射失败一律退化为 [NoopVersionMatcher] 行为(current=null → matches=true)。 + */ +object MinecraftVersionMatcher : VersionMatcher { + + @Volatile private var resolved: String? = null + @Volatile private var probed = false + + override fun current(): String? { + if (probed) return resolved + synchronized(this) { + if (probed) return resolved + resolved = try { + val cls = Class.forName("taboolib.module.nms.MinecraftVersion", false, javaClass.classLoader) + val instance = cls.getField("INSTANCE").get(null) + // universal craftbukkit / remap-only 环境下 minecraftVersion=UNKNOWN, + // 此时不把 runningVersion 视作"可稳定匹配的 NMS 版本",退回 null => matches=true。 + val mcVersion = tryRead(cls, instance, "minecraftVersion") + if (mcVersion == null) null else tryRead(cls, instance, "runningVersion") ?: mcVersion + } catch (_: Throwable) { + null + } + probed = true + Forensics.debug("MinecraftVersionMatcher: current=$resolved") + return resolved + } + } + + private fun tryRead(cls: Class<*>, instance: Any, name: String): String? { + for (getter in listOf(name, "get${name.replaceFirstChar { it.uppercase() }}")) { + try { + val v = cls.getMethod(getter).invoke(instance)?.toString() + if (!v.isNullOrBlank() && v != "UNKNOWN") return v + } catch (_: Throwable) {} + } + try { + val v = cls.getField(name).get(instance)?.toString() + if (!v.isNullOrBlank() && v != "UNKNOWN") return v + } catch (_: Throwable) {} + return null + } +} + +/** + * 全局 matcher 仓库 —— 按 FQCN 缓存外部用户自定义匹配器实例。 + * + * 解析顺序: + * 1. FQCN 为空 → [MinecraftVersionMatcher] + * 2. 找到 Kotlin object(INSTANCE 字段)→ 复用 + * 3. 否则尝试无参构造 + * 4. 全部失败 → 警告并回退 [NoopVersionMatcher] + */ +object VersionMatchers { + + private val cache = java.util.concurrent.ConcurrentHashMap() + + fun resolve(fqcn: String): VersionMatcher { + if (fqcn.isBlank()) return MinecraftVersionMatcher + cache[fqcn]?.let { return it } + val v = try { + val cls = loadClass(fqcn) + val obj = runCatching { cls.getField("INSTANCE").get(null) as? VersionMatcher }.getOrNull() + obj ?: (cls.getDeclaredConstructor().apply { isAccessible = true }.newInstance() as VersionMatcher) + } catch (t: Throwable) { + Forensics.warn("VersionMatcher 解析失败: $fqcn — ${t.javaClass.name}: ${t.message}; 回退 NoopVersionMatcher") + NoopVersionMatcher + } + cache[fqcn] = v + return v + } + + /** + * 多 ClassLoader 探查 —— 顺序:thread context → VersionMatchers 自身 CL → system CL。 + * + * 必要性:incision 模块由 IsolatedClassLoader 加载,看不到承载它的插件类。 + * 用户在自己插件里写 @Version(matcher = "com.foo.MyMatcher") 时,MyMatcher 在插件 CL, + * 必须穿透 system / context CL 才能解析到。 + */ + private fun loadClass(fqcn: String): Class<*> { + val candidates = sequenceOf( + Thread.currentThread().contextClassLoader, + VersionMatchers::class.java.classLoader, + ClassLoader.getSystemClassLoader(), + ).filterNotNull().distinct() + var lastError: Throwable? = null + for (cl in candidates) { + try { + return Class.forName(fqcn, true, cl) + } catch (t: Throwable) { + lastError = t + } + } + throw lastError ?: ClassNotFoundException(fqcn) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/cache/IncisionCache.kt b/module/incision/src/main/kotlin/taboolib/module/incision/cache/IncisionCache.kt new file mode 100644 index 000000000..094f45a3a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/cache/IncisionCache.kt @@ -0,0 +1,93 @@ +package taboolib.module.incision.cache + +import taboolib.module.incision.diagnostic.Forensics +import java.io.File +import java.security.MessageDigest + +/** + * 字节码缓存 — 仿 `taboolib.module.nms.AsmClassTranslation` + `BinaryCache` 的语义。 + * + * 缓存键 = `srcDigest + adviceConfigDigest + envSignature`,三者任一改变缓存即失效。 + * 缓存目录默认 `/cache/incision/`。 + * + * 使用约定: + * - weaver 在第一次织入某个目标类之前先查 [get] + * - 织入完成后写回 [put] + * - 命中时直接复用缓存字节,跳过 ASM 流程 + */ +object IncisionCache { + + private val md = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") } + + /** 缓存目录;可通过系统属性 `incision.cacheDir` 覆盖 */ + @Volatile + var cacheDir: File = File(System.getProperty("incision.cacheDir") ?: "cache/incision").also { + runCatching { it.mkdirs() } + } + + /** 是否启用磁盘缓存;默认开启,调试期可关 */ + @Volatile + var enabled: Boolean = System.getProperty("incision.cache", "true").toBoolean() + + fun digest(bytes: ByteArray): String { + val d = md.get() + d.reset() + return d.digest(bytes).joinToString("") { "%02x".format(it) } + } + + fun digest(text: String): String = digest(text.toByteArray(Charsets.UTF_8)) + + /** 复合 key */ + fun keyOf(srcDigest: String, adviceConfigDigest: String, envSignature: String): String = + digest("$srcDigest|$adviceConfigDigest|$envSignature") + + fun get(key: String): ByteArray? { + if (!enabled) return null + val f = file(key) + return try { + if (f.isFile) f.readBytes() else null + } catch (t: Throwable) { + Forensics.warn("IncisionCache get $key failed: ${t.message}") + null + } + } + + fun put(key: String, bytes: ByteArray) { + if (!enabled) return + val f = file(key) + try { + f.parentFile?.mkdirs() + f.writeBytes(bytes) + } catch (t: Throwable) { + Forensics.warn("IncisionCache put $key failed: ${t.message}") + } + } + + fun invalidate(key: String): Boolean { + val f = file(key) + return f.isFile && f.delete() + } + + fun clear() { + try { + cacheDir.walkTopDown().filter { it.isFile && it.name.endsWith(".class") } + .forEach { it.delete() } + } catch (t: Throwable) { + Forensics.warn("IncisionCache clear failed: ${t.message}") + } + } + + /** 开发模式 dump — 把 before/after 写到 `/.dev/incision//` */ + fun dump(devDir: File, id: String, before: ByteArray, after: ByteArray) { + try { + val dir = File(devDir, ".dev/incision/${safe(id)}").apply { mkdirs() } + File(dir, "before.class").writeBytes(before) + File(dir, "after.class").writeBytes(after) + } catch (t: Throwable) { + Forensics.warn("IncisionCache dump $id failed: ${t.message}") + } + } + + private fun safe(s: String): String = s.replace(Regex("[^A-Za-z0-9._\\-#]"), "_") + private fun file(key: String): File = File(cacheDir, "${key.take(2)}/$key.class") +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/command/IncisionCommandHandler.kt b/module/incision/src/main/kotlin/taboolib/module/incision/command/IncisionCommandHandler.kt new file mode 100644 index 000000000..ed8342e64 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/command/IncisionCommandHandler.kt @@ -0,0 +1,71 @@ +package taboolib.module.incision.command + +import taboolib.module.incision.dsl.Scalpel +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher + +/** + * 不强依赖 TabooLib 命令系统的"命令处理器"。 + * + * 调用方在自己插件里挂 TabooLib `@CommandHeader`,把子命令路由到本对象的方法。 + * 这样 incision 模块本身仍 platform-agnostic。 + * + * 子命令: + * - `list` 列出所有切术 + * - `show ` 打印某切术详情 + * - `heal ` 卸载该切术 + * - `dump` 打印 dispatcher 链快照 + * - `plugins` 按 pluginName 分组展示 advice + */ +object IncisionCommandHandler { + + fun list(): String = buildString { + val all = SurgeryRegistry.list() + appendLine("[Incision] sutures (${all.size}):") + for (s in all) { + appendLine(" ${s.id} state=${s.state} targets=${s.targets.size}") + } + } + + fun show(id: String): String { + val s = Scalpel.find(id) ?: return "[Incision] not found: $id" + return buildString { + appendLine("[Incision] $id") + appendLine(" state : ${s.state}") + appendLine(" holder: ${s.holder.qualifiedName}") + for (t in s.targets) { + appendLine(" target: ${t.signature}") + val chain = TheatreDispatcher.chainOf(t).list() + for (e in chain) { + appendLine(" advice id=${e.id} kind=${e.kind} pri=${e.priority} enabled=${e.enabled}") + } + } + } + } + + fun heal(id: String): String { + val s = Scalpel.find(id) ?: return "[Incision] not found: $id" + return if (s.heal()) "[Incision] healed $id" else "[Incision] heal failed (already healed?)" + } + + fun dump(): String = buildString { + appendLine("[Incision] dispatcher dump:") + for (s in SurgeryRegistry.list()) { + for (t in s.targets) { + val chain = TheatreDispatcher.chainOf(t).list() + if (chain.isEmpty()) continue + appendLine(" ${t.signature} (${chain.size} advice)") + for (e in chain) appendLine(" - ${e.id} kind=${e.kind} pri=${e.priority}") + } + } + } + + fun plugins(): String = buildString { + val byPlugin = SurgeryRegistry.list().groupBy { it.id.substringBefore('#').substringBefore('.', "?") } + appendLine("[Incision] sutures by holder package (${byPlugin.size}):") + for ((k, v) in byPlugin) { + appendLine(" $k: ${v.size}") + for (s in v) appendLine(" - ${s.id}") + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Checkup.kt b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Checkup.kt new file mode 100644 index 000000000..b622c24d6 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Checkup.kt @@ -0,0 +1,40 @@ +package taboolib.module.incision.diagnostic + +import taboolib.module.incision.runtime.SurgeryRegistry + +/** + * 启动期一次性体检 — 汇总所有已注册切术,输出一张表格。 + * + * 实际的"扫描 @Surgeon / @SurgeryDesk"工作由 TabooLib 自身的 @Awake / @Inject + * 机制完成(object 被触达时 provideDelegate 触发注册)。本类负责在启动期、 + * advice 完成注册后做一次汇总与错误收集,不做重复扫描。 + */ +object Checkup { + + /** 启动期执行:汇总所有 suture,打印一张可读表格。 */ + fun runStartupCheckup() { + if (!Forensics.DEBUG) return + val all = SurgeryRegistry.list() + if (all.isEmpty()) { + Forensics.info("Checkup: 未发现任何 incision 切术(等待 SurgeryDesk 触达)") + return + } + val rows = all.map { s -> + Row(s.id, s.targets.joinToString(",") { it.signature }, s.state.name) + } + val lines = buildString { + for (r in rows) { + appendLine("- ${r.id}") + appendLine(" state: ${r.state}") + appendLine(" target: ${shorten(r.target)}") + } + } + Forensics.info("Checkup report: total=${all.size}\n$lines") + } + + private fun shorten(text: String, limit: Int = 160): String { + return if (text.length <= limit) text else text.take(limit - 1) + "…" + } + + private data class Row(val id: String, val target: String, val state: String) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/ConflictAnalyzer.kt b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/ConflictAnalyzer.kt new file mode 100644 index 000000000..9cac3b6ca --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/ConflictAnalyzer.kt @@ -0,0 +1,73 @@ +package taboolib.module.incision.diagnostic + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher + +/** + * 冲突分析器 — 在每次 register 时跑一遍,检查同一目标的 advice 组合是否存在已知风险。 + * + * 规则: + * - **同一 target 上有 ≥2 个 [AdviceKind.EXCISE]** → 强制 [Trauma.Conflict.MultipleExcise] + * - **EXCISE + 任意非 LEAD/TRAIL** → 警告(链中后续 advice 可能不可达) + * - **多个 BYPASS** → 警告(链式 resume 行为依赖 priority 顺序,易出错) + * + * Lead/Trail/Splice 多对一不警告;它们设计上就支持叠加。 + */ +object ConflictAnalyzer { + + data class Report(val target: MethodCoordinate, val severity: Severity, val message: String, val involved: List) + + enum class Severity { ERROR, WARN, INFO } + + /** 分析当前已注册 chain,返回所有报告。 */ + fun analyze(target: MethodCoordinate): List { + val entries = TheatreDispatcher.chainOf(target).list() + if (entries.size < 2) return emptyList() + val reports = mutableListOf() + val excises = entries.filter { it.kind == AdviceKind.EXCISE } + val bypasses = entries.filter { it.kind == AdviceKind.BYPASS } + if (excises.size > 1) { + reports += Report(target, Severity.ERROR, "同一 target 有 ${excises.size} 个 @Excise — 仅允许一个", excises) + } + if (excises.isNotEmpty() && entries.size > excises.size) { + val others = entries.filter { it.kind !in setOf(AdviceKind.LEAD, AdviceKind.TRAIL, AdviceKind.EXCISE) } + if (others.isNotEmpty()) { + reports += Report(target, Severity.WARN, "@Excise 与 ${others.joinToString { it.kind.name }} 共存,后者可能不可达", others + excises) + } + } + if (bypasses.size > 1) { + reports += Report(target, Severity.WARN, "同一 target 有 ${bypasses.size} 个 @Bypass,链式调用可能产生循环或顺序歧义", bypasses) + } + return reports + } + + /** 全量分析 — 对所有 target 跑一遍 */ + fun analyzeAll(): List { + val all = mutableListOf() + SurgeryRegistry.list().flatMap { it.targets }.distinct().forEach { all += analyze(it) } + return all + } + + /** 出错 → 抛 Trauma;警告 → Forensics.warn;info → Forensics.info */ + fun emit(report: Report) { + when (report.severity) { + Severity.ERROR -> { + if (report.message.contains("Excise")) { + throw Trauma.Conflict.MultipleExcise(report.target, report.involved.map { it.id }) + } + Forensics.error("[Conflict] ${report.target} ${report.message}") + } + Severity.WARN -> { + if (report.message.contains("Bypass")) { + Forensics.warn("[Conflict] ${report.target} ${report.message} ids=${report.involved.map { it.id }}") + } else { + Forensics.warn("[Conflict] ${report.target} ${report.message} ids=${report.involved.map { it.id }}") + } + } + Severity.INFO -> Forensics.info("[Conflict] ${report.target} ${report.message}") + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt new file mode 100644 index 000000000..0969ae722 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt @@ -0,0 +1,54 @@ +package taboolib.module.incision.diagnostic + +/** + * 结构化诊断日志器。 + * + * 所有 incision 内部日志都走该入口,固定字段格式便于 grep: + * `[Incision][] id=... target=... resolver=... result=... took=...ms` + * + * 当前为最小实现 — 直接 println;后续接入 TabooLib `info/warning/severe`。 + */ +object Forensics { + + /** 是否开启调试输出 — 通过 JVM 参数 -Dtaboolib.incision.debug=true 或环境变量 INCISION_DEBUG=1 启用 */ + val DEBUG: Boolean by lazy { + System.getProperty("taboolib.incision.debug")?.equals("true", true) == true + || System.getenv("INCISION_DEBUG") == "1" + } + + fun info(message: String) { + println("[Incision] $message") + } + + fun debug(message: String) { + if (DEBUG) println("[Incision][DEBUG] $message") + } + + fun warn(message: String) { + println("[Incision][WARN] $message") + } + + fun error(message: String, cause: Throwable? = null) { + System.err.println("[Incision][ERROR] $message") + cause?.printStackTrace(System.err) + } + + /** + * 上报 Trauma — 输出结构化字段 + 触发栈。 + */ + fun report(trauma: Trauma) { + System.err.println(buildString { + append("[Incision][Trauma][").append(trauma.phase).append("] ") + trauma.incisionId?.let { append("id=").append(it).append(' ') } + trauma.target?.let { append("target=").append(it).append(' ') } + trauma.surgeonClass?.let { append("surgeon=").append(it).append(' ') } + trauma.resolverName?.let { append("resolver=").append(it).append(' ') } + append("\n reason: ").append(trauma.message) + var c = trauma.cause + while (c != null) { + append("\n cause : ").append(c) + c = c.cause + } + }) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Trauma.kt b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Trauma.kt new file mode 100644 index 000000000..08ecd0259 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Trauma.kt @@ -0,0 +1,222 @@ +package taboolib.module.incision.diagnostic + +import taboolib.module.incision.api.MethodCoordinate + +/** + * Incision 错误根类 — 所有 incision 故障走此家族,便于统一 Forensics 结构化日志。 + * + * 切术复杂度高,禁止抛 `NullPointerException` 或裸异常。每个场景都有明确子类, + * 附带足够上下文以便"一眼看出发生了什么"。 + * + * @property phase 阶段:DECLARATION / RESOLUTION / WEAVING / RUNTIME / ATTACH + * @property incisionId 切术 id(若可识别,`FQCN#name`) + * @property target 目标坐标(若已解析) + * @property surgeonClass 声明所在类 FQCN + * @property rawDescriptor 转译前描述符 + * @property translatedDescriptor 转译后描述符 + * @property resolverName NameResolver 类名 + * @property resolverEnv NameResolver 环境签名 + */ +sealed class Trauma( + val phase: Phase, + message: String, + cause: Throwable? = null, + val incisionId: String? = null, + val target: MethodCoordinate? = null, + val surgeonClass: String? = null, + val rawDescriptor: String? = null, + val translatedDescriptor: String? = null, + val resolverName: String? = null, + val resolverEnv: String? = null, +) : RuntimeException(message, cause) { + + enum class Phase { DECLARATION, RESOLUTION, WEAVING, RUNTIME, ATTACH, CONFLICT } + + // --------------------------------------------------------------------- + // Declaration — 声明期(@Surgeon / @SurgeryDesk 扫描) + // --------------------------------------------------------------------- + sealed class Declaration(message: String, cause: Throwable? = null, incisionId: String? = null, surgeonClass: String? = null) + : Trauma(Phase.DECLARATION, message, cause, incisionId, surgeonClass = surgeonClass) { + + class InvalidHolder(clazz: String, kindActual: String) + : Declaration("@SurgeryDesk / @Surgeon 必须标注在 object 上,检测到 $kindActual '$clazz'", surgeonClass = clazz) + + class DuplicateId(id: String, existing: String) + : Declaration("切术 ID 重复: $id (已由 $existing 占用)", incisionId = id) + + class BadDescriptor(raw: String, reason: String, cause: Throwable? = null) + : Declaration("描述符无法解析: '$raw',原因: $reason。期望格式: 类#方法名(参数)返回", cause) + + class BadScope(raw: String, position: Int, reason: String) + : Declaration("Scope DSL 语法错误,位置 $position 附近: $reason。原文: '$raw'") + + class IllegalSignature(id: String, reason: String) + : Declaration("Handler 签名非法: $reason", incisionId = id) + } + + // --------------------------------------------------------------------- + // Resolution — 目标解析 + // --------------------------------------------------------------------- + sealed class Resolution(message: String, cause: Throwable? = null, incisionId: String? = null, + rawDescriptor: String? = null, translatedDescriptor: String? = null, + resolverName: String? = null, resolverEnv: String? = null) + : Trauma(Phase.RESOLUTION, message, cause, incisionId, + rawDescriptor = rawDescriptor, translatedDescriptor = translatedDescriptor, + resolverName = resolverName, resolverEnv = resolverEnv) { + + class ClassNotFound(id: String, lastTriedName: String, resolverName: String, resolverEnv: String, cause: Throwable? = null) + : Resolution("目标类 $lastTriedName 不存在(NameResolver=$resolverName,env=$resolverEnv)", + cause, id, resolverName = resolverName, resolverEnv = resolverEnv) + + class MethodNotFound(id: String, owner: String, nameAndDesc: String, candidates: List) + : Resolution("$owner 上不存在方法 $nameAndDesc;候选: ${candidates.joinToString(prefix = "[", postfix = "]")}", incisionId = id) + + class AmbiguousTarget(id: String, scope: String, matches: List) + : Resolution("scope 匹配到多个方法 (${matches.size}),需细化: '$scope' → ${matches.take(5)}", incisionId = id) + + class RemapMismatch(id: String, expected: String, actual: String, env: String) + : Resolution("Mojang/Spigot 映射表差异 (expected=$expected, actual=$actual, env=$env)", incisionId = id) + } + + // --------------------------------------------------------------------- + // Weaving — 字节码织入 + // --------------------------------------------------------------------- + sealed class Weaving(message: String, cause: Throwable? = null, incisionId: String? = null, target: MethodCoordinate? = null) + : Trauma(Phase.WEAVING, message, cause, incisionId, target = target) { + + class FrameMismatch(id: String, target: MethodCoordinate, cause: Throwable? = null) + : Weaving("插入 advice 后 StackMapFrame 不匹配,方法=$target", cause, id, target) + + class UnsupportedConstruct(id: String, target: MethodCoordinate, reason: String) + : Weaving("无法对 $reason 方法织入: $target", incisionId = id, target = target) + + class AsmVerifyError(id: String, target: MethodCoordinate, verifyOutput: String, cause: Throwable? = null) + : Weaving("ClassWriter 验证失败,target=$target\n$verifyOutput", cause, id, target) + + class RetransformRejected(id: String, target: MethodCoordinate, cause: Throwable? = null) + : Weaving("JVM 拒绝 retransformClasses(可能改动了 schema),target=$target", cause, id, target) + } + + // --------------------------------------------------------------------- + // Runtime — dispatcher 调用期 + // --------------------------------------------------------------------- + sealed class Runtime(message: String, cause: Throwable? = null, incisionId: String? = null, target: MethodCoordinate? = null) + : Trauma(Phase.RUNTIME, message, cause, incisionId, target = target) { + + class HandlerThrew(id: String, target: MethodCoordinate, cause: Throwable) + : Runtime("施术方法抛出异常 (advice=$id, target=$target)", cause, id, target) + + class ResumeMissing(id: String, target: MethodCoordinate) + : Runtime("@Splice 未调用 Theatre.resume.proceed() 且无返回值,advice=$id", incisionId = id, target = target) + + class ArgCoercionFailed(id: String, expected: String, actual: String, cause: Throwable? = null) + : Runtime("参数类型转换失败(期望 $expected,实际 $actual)", cause, id) + + class Unresolved(id: String, target: MethodCoordinate?, reason: String) + : Runtime("运行期访问未解析的切术 $id: $reason", incisionId = id, target = target) + } + + // --------------------------------------------------------------------- + // Accessor — handler 内读写字段 / 调用方法 + // --------------------------------------------------------------------- + sealed class Accessor(message: String, cause: Throwable? = null) + : Trauma(Phase.RUNTIME, message, cause) { + + class FieldNotFound(ownerClass: String, name: String) + : Accessor("找不到字段: $ownerClass.$name(已沿继承链向上查找)") + + class MethodNotFound(ownerClass: String, name: String, descriptor: String?, candidates: List) + : Accessor("找不到方法: $ownerClass.$name${descriptor ?: "(按参数类型匹配)"};候选: ${candidates.joinToString(prefix = "[", postfix = "]")}") + + class AmbiguousMethod(ownerClass: String, name: String, matches: List) + : Accessor("方法重载匹配到多个: $ownerClass.$name → ${matches.joinToString(prefix = "[", postfix = "]")};请显式传入 descriptor") + + class AccessDenied(ownerClass: String, member: String, reason: String, cause: Throwable? = null) + : Accessor("无法访问 $ownerClass.$member:$reason", cause) + + class StaticOnInstance(member: String) + : Accessor("试图在静态方法的 self 上读写实例字段: $member;请改用 staticField(...) 或传入显式 receiver") + } + + // --------------------------------------------------------------------- + // Attach — self-attach + // --------------------------------------------------------------------- + sealed class Attach(message: String, cause: Throwable? = null) + : Trauma(Phase.ATTACH, message, cause) { + + class NoToolsJar(javaHome: String) + : Attach("JDK 8 attach 失败:JAVA_HOME=$javaHome 下无 tools.jar;" + + "请使用 JDK 而非 JRE,或引入 byte-buddy-agent 依赖") + + class AttachSelfDisabled + : Attach("jdk.attach.allowAttachSelf=false;请通过 -Djdk.attach.allowAttachSelf=true 启用," + + "或在启动参数中加入 byte-buddy-agent") + + class AgentLoadFailed(agentJar: String, cause: Throwable) + : Attach("attach 成功但 loadAgent 失败: $agentJar", cause) + + class ReflectionBlocked(module: String, cause: Throwable) + : Attach("JDK 17+ 模块限制,需要 --add-opens $module", cause) + + class InstrumentationUnavailable(reason: String) + : Attach("无法获取 Instrumentation:$reason") + } + + // --------------------------------------------------------------------- + // Conflict — 多插件冲突 + // --------------------------------------------------------------------- + sealed class Conflict(message: String, cause: Throwable? = null, target: MethodCoordinate? = null) + : Trauma(Phase.CONFLICT, message, cause, target = target) { + + class MultipleExcise(target: MethodCoordinate, offenders: List) + : Conflict("同一 target 存在多个 @Excise:$target\n ${offenders.joinToString("\n ")}", target = target) + + class BypassOverlap(target: MethodCoordinate, offenders: List) + : Conflict("多个 @Bypass 在同一 target 上,链式顺序可能不符预期:$target\n ${offenders.joinToString("\n ")}", target = target) + + class GateVersionMismatch(selfApi: Int, gateApi: Int) + : Conflict("本插件 incision api=$selfApi,在线网关 api=$gateApi;已降级到共有功能子集") + + class ClassLoaderLeaked(pluginName: String, incisionId: String) + : Conflict("advice 的 classLoader 已回收但未 heal:plugin=$pluginName id=$incisionId") + } + + // --------------------------------------------------------------------- + // Predicate — 谓词 DSL(解析 / 编译 / 运行) + // --------------------------------------------------------------------- + sealed class Predicate(message: String, cause: Throwable? = null, incisionId: String? = null) + : Trauma(Phase.DECLARATION, message, cause, incisionId) { + + /** 词法 / 语法错误,position 为源码字符偏移(0 起算),-1 表示未知。 */ + class SyntaxError(val source: String, val position: Int, reason: String) + : Predicate("谓词语法错误,位置 $position 附近: $reason\n 源码: $source") + + /** 引用了未定义变量(不在 args/this/result/env/site/caller 等已知绑定中)。 */ + class UndefinedVariable(val source: String, val name: String) + : Predicate("谓词引用了未定义变量 '$name'\n 源码: $source") + + /** 在方法调用结果上做下标访问(不允许 args.size[0] 这类形式)。 */ + class MethodIndexed(val source: String, val method: String) + : Predicate("谓词不允许对方法调用结果再做下标访问: $method[]\n 源码: $source") + + /** 访问了未知成员(属性 / 方法),用于编译期与运行期共用。 */ + class UnknownMember(val source: String, val owner: String, val member: String) + : Predicate("谓词访问了未知成员: $owner.$member\n 源码: $source") + + /** 类型不匹配(含 as/is/ic/ip/it 失败的强制语义、比较两侧类型不可比等)。 */ + class TypeMismatch(val source: String, reason: String) + : Predicate("谓词类型不匹配: $reason\n 源码: $source") + + /** 运行期捕获的兜底异常,被 dispatcher 包装。 */ + class RuntimeFailure(val source: String, val adviceId: String?, cause: Throwable) + : Predicate("谓词运行期异常 (advice=$adviceId): ${cause.message}\n 源码: $source", cause, adviceId) + } + + // --------------------------------------------------------------------- + // DSL 非法调用 + // --------------------------------------------------------------------- + class IllegalCallSite(reason: String, stacktrace: List) + : Trauma(Phase.DECLARATION, + "scalpel.transient / DSL 必须在 @SurgeryDesk object 内部调用:$reason\n" + + "调用栈:\n ${stacktrace.joinToString("\n ")}") +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ArmTrigger.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ArmTrigger.kt new file mode 100644 index 000000000..784ea6620 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ArmTrigger.kt @@ -0,0 +1,43 @@ +package taboolib.module.incision.dsl + +import taboolib.module.incision.api.Suture + +/** + * `scalpel.armOn(...)` / `scalpel.disarmOn(...)` 的返回句柄。 + * + * 持有 builder + 目标事件类型 + 当前 arm 状态;用户在 SurgeryDesk 内接 TabooLib + * 的 `@SubscribeEvent` 将该事件路由到 [arm] / [disarm]。 + * + * 不将 incision 耦合到 Bukkit 事件体系,保持核心 platform 无关。 + */ +class ArmTrigger internal constructor( + val eventClass: Class<*>, + private val block: ScalpelBuilder.() -> Unit, + defaultArmed: Boolean, +) : AutoCloseable { + + @Volatile + private var suture: Suture? = null + + init { + if (defaultArmed) arm() + } + + fun arm(): Suture { + suture?.let { if (it.state != Suture.State.HEALED) return it } + val s = Scalpel.transient(block) + suture = s + return s + } + + fun disarm(): Boolean { + val s = suture ?: return false + val ok = s.heal() + suture = null + return ok + } + + fun isArmed(): Boolean = suture?.state == Suture.State.ARMED + + override fun close() { disarm() } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt new file mode 100644 index 000000000..50546c3c6 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt @@ -0,0 +1,279 @@ +package taboolib.module.incision.dsl + +import taboolib.module.incision.annotation.SurgeryDesk +import taboolib.module.incision.api.Suture +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.Backend +import taboolib.module.incision.loader.InstrumentationBackend +import taboolib.module.incision.loader.JvmtiBackend +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher +import taboolib.module.incision.remap.RemapRouter +import taboolib.module.incision.weaver.Scalpel as ScalpelWeaver +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * DSL 顶层入口 — + * + * ``` + * @SurgeryDesk + * object MyPatches { + * val tracker: Suture by scalpel { + * lead("org.bukkit.entity.Player#kickPlayer(*)") { theatre -> ... } + * } + * + * fun onDemand() { + * scalpel.transient { + * splice("...") { ... } + * }.use { runBackup() } + * } + * } + * ``` + */ +object Scalpel { + + operator fun invoke(block: ScalpelBuilder.() -> Unit): ScalpelProvider = + ScalpelProvider(block, deferred = false, eagerArm = true) + + /** 惰性切 — 属性首次被访问或目标类加载时物理织入 */ + fun deferred(block: ScalpelBuilder.() -> Unit): ScalpelProvider = + ScalpelProvider(block, deferred = true, eagerArm = false) + + /** 作用域切 — 在块内生效,块外自动 heal */ + fun scoped(block: ScalpelBuilder.() -> Unit): ScopedHandle { + verifyCallerIsSurgeryDesk("scoped") + return ScopedHandle(block) + } + + /** 线程局部切 — 默认 disable,按线程手动 activate */ + fun threadLocal(block: ScalpelBuilder.() -> Unit): ThreadLocalSuture { + verifyCallerIsSurgeryDesk("threadLocal") + return ThreadLocalSuture(block) + } + + /** + * 事件驱动 — 监听到 [eventClass] 实例时才 arm advice。 + * 真正的事件订阅由调用方在 SurgeryDesk 内自行接 TabooLib `@SubscribeEvent`, + * 此处仅返回一个 [ArmTrigger],由用户主动 [ArmTrigger.arm] / [ArmTrigger.disarm]。 + * + * 设计上:incision 不强行把 Bukkit 事件耦合进核心,避免 platform 依赖污染。 + */ + fun armOn(eventClass: Class<*>, block: ScalpelBuilder.() -> Unit): ArmTrigger { + verifyCallerIsSurgeryDesk("armOn") + return ArmTrigger(eventClass, block, defaultArmed = false) + } + + fun disarmOn(eventClass: Class<*>, block: ScalpelBuilder.() -> Unit): ArmTrigger { + verifyCallerIsSurgeryDesk("disarmOn") + return ArmTrigger(eventClass, block, defaultArmed = true) + } + + /** + * 互斥块 — 块内的切术替换同一目标的其他活跃切术;块结束后恢复。 + * 实现:进入时 suspend 同 target 上其他 ARMED suture,退出时 resume。 + */ + fun exclusive(block: ScalpelBuilder.() -> Unit, body: () -> R): R { + verifyCallerIsSurgeryDesk("exclusive") + val suture = transient(block) + val touched = mutableListOf() + try { + for (t in suture.targets) { + for (other in SurgeryRegistry.listByTarget(t)) { + if (other === suture) continue + if (other.state == taboolib.module.incision.api.Suture.State.ARMED) { + if (other.suspend()) touched += other + } + } + } + return body() + } finally { + for (s in touched) s.resume() + suture.heal() + } + } + + /** 复制句柄结构用于 A/B — 用同一 builder 再注册一份独立的临时 suture */ + fun fork(suture: Suture, mark: String = "fork"): Suture? { + verifyCallerIsSurgeryDesk("fork") + // fork 仅复制声明,不触发原 suture 的 advice + Forensics.warn("scalpel.fork(${suture.id}) — 当前实现仅复制 id 标记 '$mark',请用 transient { } 重写副本") + return null + } + + /** A/B 重放 — 等价于把 suture 的 advice 复制到一个临时 suture */ + fun replay(suture: Suture): Suture? = fork(suture, "replay") + + /** 全局查询 */ + fun find(id: String): Suture? = SurgeryRegistry.find(id) + + /** 列出所有切术;支持按 holder 或 target 过滤 */ + fun list( + holder: kotlin.reflect.KClass<*>? = null, + target: String? = null, + ): List = SurgeryRegistry.list().filter { s -> + (holder == null || s.holder == holder) && + (target == null || s.targets.any { it.signature.contains(target) }) + } + + /** 按 scope 子串卸载所有匹配的 suture */ + fun healAll(scope: String? = null): Int { + val targets = SurgeryRegistry.list().filter { + scope == null || it.id.contains(scope) || it.targets.any { t -> t.signature.contains(scope) } + } + var n = 0 + for (s in targets) if (s.heal()) n++ + return n + } + + /** 压缩 — 移除所有 HEALED 状态的孤儿条目(当前 SurgeryRegistry 已自动清,留为占位) */ + fun compact(): Int { + // SurgeryRegistry.unregister 已在 heal 时调用,无需额外 compact;预留接口 + return 0 + } + + /** 临时切 — 返回 AutoCloseable,调用点必须在 @SurgeryDesk object 内部 */ + fun transient(block: ScalpelBuilder.() -> Unit): Suture { + val caller = verifyCallerIsSurgeryDesk("transient") + val seq = TransientCounter.next() + val id = "${caller.name}#anon-$seq" + val builder = ScalpelBuilder().apply(block) + val holder = try { + caller.getDeclaredField("INSTANCE").apply { isAccessible = true }.get(null) + } catch (t: Throwable) { + throw Trauma.Declaration.InvalidHolder(caller.name, "transient 调用点必须在 object 中") + } + val (targets, entries) = builder.materialize(id, holder!!) + for (e in entries) TheatreDispatcher.register(e) + installWeaver(entries) + val kclass = caller.kotlin + val suture = SutureImpl(id, targets, kclass, entries) + SurgeryRegistry.register(id, suture) + return suture + } + + /** + * 安装字节码 weaver — 把目标 owner 注册到 InstrumentationBackend, + * 触发一次 retransform,使已加载的目标类被注入 dispatcher 调用。 + * + * 同一 owner 多次注册会累积所有 entries 的 weaver;retransform 是幂等的。 + */ + private val accumulatedTargets = java.util.concurrent.ConcurrentHashMap>() + private val activeTokens = java.util.concurrent.ConcurrentHashMap() + + internal fun installWeaver(entries: List) { + if (entries.isEmpty()) return + // Kotlin @JvmStatic 方法有两条调用路径: + // 1. 外部类的 static bridge(Java 调用者) + // 2. $Companion 的 instance method(Kotlin 调用者) + // 自动扩展 companion 目标,确保两条路径都被织入 + val existingOwners = entries.map { it.target.owner }.toSet() + val expanded = mutableListOf() + for (e in entries) { + expanded += e + if (!e.target.owner.endsWith("\$Companion")) { + val companionOwner = e.target.owner + "\$Companion" + // 避免与 SurgeonScanner.expandTargets 重复 + if (companionOwner !in existingOwners) { + val companionTarget = e.target.copy(owner = companionOwner) + expanded += e.copy( + id = e.id + "#companion", + target = companionTarget, + ) + } + } + } + // 注册 companion 扩展条目到 dispatcher + for (e in expanded) { + if (e.id.endsWith("#companion")) { + TheatreDispatcher.register(e) + } + } + val backend = resolveBackend() + val byOwner = expanded.groupBy { it.target.owner } + for ((owner, group) in byOwner) { + // NMS remap:把用户写的 net/minecraft/server/MinecraftServer + // 转换为运行时实际类名 net/minecraft/server/v1_12_R1/MinecraftServer + val resolvedOwner = RemapRouter.resolveOwner(owner) + val newTargets = group.groupBy { it.target }.map { (target, targetEntries) -> + ScalpelWeaver.AdviceTargetSpec( + target = target, + kinds = targetEntries.map { it.kind }.toSet(), + sites = targetEntries.mapNotNull { it.siteSpec }, + ) + } + val allTargets = accumulatedTargets.computeIfAbsent(resolvedOwner) { mutableListOf() } + allTargets.addAll(newTargets) + activeTokens.remove(resolvedOwner)?.remove() + val weaver = ScalpelWeaver(targetsByOwner = mapOf(resolvedOwner to allTargets.toList())) + val token = backend.addTransformer(resolvedOwner) { bytes -> weaver.weave(bytes) } + activeTokens[resolvedOwner] = token + backend.retransform(resolvedOwner.replace('/', '.')) + Forensics.debug("installWeaver owner=$owner resolved=$resolvedOwner advices=${group.size} total=${allTargets.size} backend=${backend.name}") + } + } + + private fun resolveBackend(): Backend { + if (InstrumentationBackend.available()) return InstrumentationBackend + if (JvmtiBackend.available()) return JvmtiBackend + Forensics.warn("无可用的 retransform 后端,织入可能失败") + return InstrumentationBackend + } + + /** + * 校验调用方是 @SurgeryDesk object。 + * 返回调用方 Class。 + */ + private fun verifyCallerIsSurgeryDesk(api: String): Class<*> { + val stack = Thread.currentThread().stackTrace + var firstExternal: String? = null + for (i in 2 until stack.size) { + val name = stack[i].className + if (name.startsWith("taboolib.module.incision.")) continue + // skip kotlin lambda/anonymous classes (contain $) and JDK internals + val baseName = name.substringBefore('$') + val cls = try { + Class.forName(baseName, false, Thread.currentThread().contextClassLoader) + } catch (_: Throwable) { + try { Class.forName(baseName, false, Scalpel::class.java.classLoader) } catch (_: Throwable) { null } + } ?: continue + if (cls.getAnnotation(SurgeryDesk::class.java) != null) return cls + if (firstExternal == null) firstExternal = name + } + throw Trauma.IllegalCallSite( + "scalpel.$api 只能在 @SurgeryDesk object 内部调用,实际在 ${firstExternal ?: "unknown"}", + stack.take(10).map { it.toString() } + ) + } +} + +/** + * provideDelegate 工厂 — 属性委托创建时捕获 thisRef 与 property.name,注册到 SurgeryRegistry。 + */ +class ScalpelProvider internal constructor( + private val block: ScalpelBuilder.() -> Unit, + private val deferred: Boolean, + private val eagerArm: Boolean, +) { + operator fun provideDelegate(thisRef: Any, property: KProperty<*>): ReadOnlyProperty { + val clazz = requireSurgeryDesk(thisRef, property.name) + val id = "${clazz.name}#${property.name}" + val builder = ScalpelBuilder().apply(block) + val (targets, entries) = builder.materialize(id, thisRef) + val suture = SutureImpl(id, targets, clazz.kotlin, entries) + SurgeryRegistry.register(id, suture) + if (!deferred && eagerArm) { + for (e in entries) TheatreDispatcher.register(e) + Scalpel.installWeaver(entries) + } + Forensics.debug("declared id=$id deferred=$deferred targets=${targets.size}") + return ReadOnlyProperty { _, _ -> suture } + } +} + +private object TransientCounter { + private val counter = java.util.concurrent.atomic.AtomicLong(0) + fun next(): Long = counter.incrementAndGet() +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScalpelBuilder.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScalpelBuilder.kt new file mode 100644 index 000000000..f755d80ff --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScalpelBuilder.kt @@ -0,0 +1,114 @@ +package taboolib.module.incision.dsl + +import taboolib.module.incision.annotation.SurgeryDesk +import taboolib.module.incision.api.DescriptorCodec +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Theatre +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.AdviceKind + +/** + * 构建期 DSL — 在 `scalpel { ... }` 块内可用。 + * + * 把用户写的 `lead("...") { ... }` 等翻译成待注册的 AdviceEntry。 + * 不直接注册,等 `provideDelegate` 拿到 id 后统一提交。 + */ +class ScalpelBuilder internal constructor() { + + internal val pending = mutableListOf() + internal var priority: Int = 0 + + fun priority(p: Int) { this.priority = p } + + fun lead(descriptor: String, handler: (Theatre) -> Unit) { + pending += PendingAdvice(AdviceKind.LEAD, descriptor, priority, handler = { handler(it) }) + } + + fun trail(descriptor: String, handler: (Theatre) -> Unit) { + pending += PendingAdvice(AdviceKind.TRAIL, descriptor, priority, handler = { handler(it) }) + } + + fun splice(descriptor: String, handler: (Theatre) -> Any?) { + pending += PendingAdvice(AdviceKind.SPLICE, descriptor, priority, handler) + } + + fun bypass(descriptor: String, handler: (Theatre) -> Any?) { + pending += PendingAdvice(AdviceKind.BYPASS, descriptor, priority, handler) + } + + fun excise(descriptor: String, handler: (Theatre) -> Any?) { + pending += PendingAdvice(AdviceKind.EXCISE, descriptor, priority, handler) + } + + /** 批量 — 在多个描述符上注册同样 advice */ + fun on(vararg descriptors: String, block: OnBlock.() -> Unit) { + val sub = OnBlock(this, descriptors.toList()) + sub.block() + } + + /** 条件谓词 — 附加到最后一条 advice */ + infix fun Unit.where(predicate: (Theatre) -> Boolean) { + val last = pending.lastOrNull() ?: return + pending[pending.lastIndex] = last.copy(predicate = predicate) + } + + class OnBlock internal constructor(val parent: ScalpelBuilder, val descriptors: List) { + fun lead(handler: (Theatre) -> Unit) = descriptors.forEach { parent.lead(it, handler) } + fun trail(handler: (Theatre) -> Unit) = descriptors.forEach { parent.trail(it, handler) } + fun splice(handler: (Theatre) -> Any?) = descriptors.forEach { parent.splice(it, handler) } + fun bypass(handler: (Theatre) -> Any?) = descriptors.forEach { parent.bypass(it, handler) } + } + + data class PendingAdvice( + val kind: AdviceKind, + val rawDescriptor: String, + val priority: Int, + val handler: (Theatre) -> Any?, + val predicate: ((Theatre) -> Boolean)? = null, + ) +} + +/** + * 校验 holder 必须是 `@SurgeryDesk` object。 + */ +internal fun requireSurgeryDesk(holder: Any, propertyName: String): Class<*> { + val clazz: Class<*> = holder.javaClass + val anno = clazz.getAnnotation(SurgeryDesk::class.java) + if (anno == null) { + throw Trauma.Declaration.InvalidHolder(clazz.name, "class (缺少 @SurgeryDesk)") + } + // object 单例特征:INSTANCE 字段存在且指向 holder + val instance = try { clazz.getDeclaredField("INSTANCE").apply { isAccessible = true }.get(null) } catch (_: Throwable) { null } + if (instance !== holder) { + throw Trauma.Declaration.InvalidHolder(clazz.name, "class (@SurgeryDesk 必须标注在 object 上,属性=$propertyName)") + } + return clazz +} + +/** + * Pending advice → AdviceEntry 列表,合成 id + 目标坐标。 + */ +internal fun ScalpelBuilder.materialize(ownerId: String, holder: Any): Pair, List> { + val targets = mutableListOf() + val entries = mutableListOf() + val cl = java.lang.ref.WeakReference(holder.javaClass.classLoader) + for ((idx, p) in pending.withIndex()) { + val parsed = DescriptorCodec.parseMethod(p.rawDescriptor) + ?: throw Trauma.Declaration.BadDescriptor(p.rawDescriptor, "缺少 '#' 或括号") + val coord = parsed.toCoordinate() + targets += coord + entries += AdviceEntry( + id = "$ownerId@$idx", + kind = p.kind, + target = coord, + priority = p.priority, + handler = p.handler, + predicate = p.predicate, + classLoader = cl, + explicitResumeRequired = false, + sourceKind = "dsl", + ) + } + return targets to entries +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScopedHandle.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScopedHandle.kt new file mode 100644 index 000000000..3d0e85066 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/ScopedHandle.kt @@ -0,0 +1,60 @@ +package taboolib.module.incision.dsl + +import taboolib.module.incision.api.Suture +import taboolib.module.incision.api.Theatre +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher + +/** + * `scalpel.scoped { }` 返回的作用域句柄。调用 [run] 进入作用域,块结束自动 heal。 + */ +class ScopedHandle internal constructor(private val block: ScalpelBuilder.() -> Unit) { + + fun run(body: () -> R): R { + val suture = Scalpel.transient(block) + return try { + body() + } finally { + suture.heal() + } + } +} + +/** + * 线程局部切术 — 所有 advice 默认附带谓词"当前线程需已 activate"。 + */ +class ThreadLocalSuture internal constructor(block: ScalpelBuilder.() -> Unit) : AutoCloseable { + + private val activeThreads: MutableSet = java.util.concurrent.ConcurrentHashMap.newKeySet() + private val suture: Suture + + init { + val builder = ScalpelBuilder().apply(block) + // 给所有 pending 加上 per-thread 谓词 + val wrapped = builder.pending.map { p -> + p.copy(predicate = { _: Theatre -> Thread.currentThread().id in activeThreads } ) + } + builder.pending.clear() + builder.pending.addAll(wrapped) + + val seq = System.nanoTime() + val callerClass = Thread.currentThread().stackTrace.firstOrNull { st -> + !st.className.startsWith("taboolib.module.incision.") && + !st.className.startsWith("java.") && + !st.className.startsWith("kotlin.") + }?.className ?: "unknown" + val id = "$callerClass#tls-$seq" + val holder = try { Class.forName(callerClass).getDeclaredField("INSTANCE").apply { isAccessible = true }.get(null) } catch (_: Throwable) { null } + ?: error("threadLocal 调用点不是 object") + val (targets, entries) = builder.materialize(id, holder) + for (e in entries) TheatreDispatcher.register(e) + suture = SutureImpl(id, targets, holder::class, entries) + SurgeryRegistry.register(id, suture) + } + + fun activateOnCurrentThread() { activeThreads.add(Thread.currentThread().id) } + fun deactivateOnCurrentThread() { activeThreads.remove(Thread.currentThread().id) } + + fun heal() = suture.heal() + override fun close() { heal() } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/SutureImpl.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/SutureImpl.kt new file mode 100644 index 000000000..9ad15988d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/SutureImpl.kt @@ -0,0 +1,52 @@ +package taboolib.module.incision.dsl + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Suture +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher +import kotlin.reflect.KClass + +/** + * Suture 默认实现 — 持有一组 advice 条目;heal 时从 dispatcher 移除。 + */ +internal class SutureImpl( + override val id: String, + override val targets: List, + override val holder: KClass<*>, + private val entries: List, +) : Suture { + + @Volatile + override var state: Suture.State = Suture.State.ARMED + + override fun heal(): Boolean { + if (state == Suture.State.HEALED) return false + for (e in entries) { + TheatreDispatcher.unregister(e.target, e.id) + // 清理自动扩展的 $Companion 条目 + if (!e.target.owner.endsWith("\$Companion")) { + val companionTarget = e.target.copy(owner = e.target.owner + "\$Companion") + TheatreDispatcher.unregister(companionTarget, e.id + "#companion") + } + } + SurgeryRegistry.unregister(id) + state = Suture.State.HEALED + return true + } + + override fun suspend(): Boolean { + if (state == Suture.State.HEALED) return false + for (e in entries) e.enabled = false + state = Suture.State.SUSPENDED + return true + } + + override fun resume(): Boolean { + if (state == Suture.State.HEALED) return false + for (e in entries) e.enabled = true + state = Suture.State.ARMED + return true + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/DefaultIncisionGate.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/DefaultIncisionGate.kt new file mode 100644 index 000000000..9d63b8297 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/DefaultIncisionGate.kt @@ -0,0 +1,141 @@ +package taboolib.module.incision.gate + +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * 默认 IncisionGate 实现。 + * + * 加载路径取决于 [GateBootstrapper]: + * - 理想路径:被推进系统 ClassLoader(通过 Instrumentation.appendToSystemClassLoaderSearch) + * - Fallback:加载在某个 incision ClassLoader 内,通过 Exchanges 共享实例;其他插件通过 + * reflective proxy 调用该实例 + * + * 本类只用 [IncisionGateApi] 的基础类型参数,避免跨 ClassLoader 类型泄漏。 + */ +class DefaultIncisionGate(private val apiVersion: Int) : IncisionGateApi { + + private val chains = ConcurrentHashMap() + private val implanted = ConcurrentHashMap() + private val seq = AtomicLong() + + override fun apiVersion(): Int = apiVersion + + override fun supportedAdviceTypes(): Set = + setOf("LEAD", "TRAIL", "SPLICE", "GRAFT", "BYPASS", "TRIM", "EXCISE") + + override fun ensureImplanted(targetSignature: String, weaver: BytecodeWeaverProxy): Boolean { + val prev = implanted.putIfAbsent(targetSignature, true) + if (prev != null) return false + // 触发 weaver.transform(调用方负责实际 retransform) + try { + weaver.transform(weaver.ownerClass(), ByteArray(0)) + } catch (t: Throwable) { + Forensics.warn("IncisionGate ensureImplanted 触发 weaver 失败: ${t.message}") + } + return true + } + + override fun register(targetSignature: String, advice: AdviceProxy): GlobalSutureToken { + val chain = chains.computeIfAbsent(targetSignature) { GlobalChain(it) } + chain.add(advice) + val token = TokenImpl(targetSignature, advice.incisionId, advice.pluginName, seq.incrementAndGet()) + // 冲突分析 + ConflictAnalyzer.analyze(chain) + return token + } + + override fun unregister(token: GlobalSutureToken): Boolean { + val chain = chains[token.targetSignature] ?: return false + return chain.removeById(token.incisionId) + } + + override fun dispatch(targetSignature: String, self: Any?, args: Array): Any? { + val chain = chains[targetSignature] ?: return null + val ordered = chain.snapshot() + if (ordered.isEmpty()) return null + // 逐条调用 advice,遇到返回 non-null 的 Bypass/Excise 即视为终止 + var finalResult: Any? = null + for (advice in ordered) { + val r = try { advice.invoke(targetSignature, self, args) } catch (t: Throwable) { + Forensics.error("[gate.dispatch] ${advice.incisionId} 抛出异常", t); null + } + if (advice.kind == "BYPASS" || advice.kind == "EXCISE" || advice.kind == "SPLICE") { + finalResult = r + } + } + return finalResult + } + + override fun listByPlugin(pluginName: String): List { + val out = mutableListOf() + for ((_, chain) in chains) { + out += chain.snapshot().filter { it.pluginName == pluginName }.map { it.incisionId } + } + return out + } + + override fun healByClassLoader(cl: ClassLoader): Int { + var n = 0 + for ((_, chain) in chains) n += chain.removeByClassLoader(cl) + return n + } + + private class GlobalChain(val targetSignature: String) { + private val entries = java.util.concurrent.CopyOnWriteArrayList() + fun add(a: AdviceProxy) { + entries.add(a) + val sorted = entries.toMutableList().apply { sortByDescending { it.priority } } + entries.clear() + entries.addAll(sorted) + } + fun removeById(id: String): Boolean = entries.removeIf { it.incisionId == id } + fun removeByClassLoader(cl: ClassLoader): Int { + var n = 0 + entries.removeIf { if (it.classLoader() === cl) { n++; true } else false } + return n + } + fun snapshot(): List = entries.toList() + } + + private data class TokenImpl( + override val targetSignature: String, + override val incisionId: String, + override val pluginName: String, + val seq: Long, + ) : GlobalSutureToken +} + +/** + * 启动期冲突分析。 + */ +object ConflictAnalyzer { + + fun analyze(chain: Any) { + // 通过反射访问 chain.snapshot(),避免耦合 DefaultIncisionGate 的内部类类型。 + val snapshotMethod = try { + chain.javaClass.getDeclaredMethod("snapshot").apply { isAccessible = true } + } catch (_: Throwable) { return } + @Suppress("UNCHECKED_CAST") + val list = snapshotMethod.invoke(chain) as? List ?: return + + val excises = list.filter { it.kind == "EXCISE" } + if (excises.size > 1) { + val target = (try { chain.javaClass.getField("targetSignature").get(chain) } catch (_: Throwable) { "?" }).toString() + Forensics.report(Trauma.Conflict.MultipleExcise( + taboolib.module.incision.api.MethodCoordinate(target, "", ""), + excises.map { "${it.pluginName}:${it.incisionId}" } + )) + } + val bypasses = list.filter { it.kind == "BYPASS" } + if (bypasses.size > 1) { + val target = (try { chain.javaClass.getField("targetSignature").get(chain) } catch (_: Throwable) { "?" }).toString() + Forensics.report(Trauma.Conflict.BypassOverlap( + taboolib.module.incision.api.MethodCoordinate(target, "", ""), + bypasses.map { "${it.pluginName}:${it.incisionId}" } + )) + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt new file mode 100644 index 000000000..736798a1d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/GateBootstrapper.kt @@ -0,0 +1,210 @@ +package taboolib.module.incision.gate + +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.InstrumentationBackend +import taboolib.platform.bukkit.Exchanges +import java.io.File +import java.io.FileOutputStream +import java.lang.instrument.Instrumentation +import java.util.jar.Attributes +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.jar.Manifest + +/** + * IncisionGate 引导器 — 决定 gate 实例的承载方式。 + * + * 优先路径:通过 [Instrumentation.appendToSystemClassLoaderSearch] 把 + * gate jar 推入系统 ClassLoader,使 `taboolib.incision.gate.IncisionGate$V` + * 成为 JVM 唯一类。 + * + * Fallback:若 Bukkit `Exchanges` 可用,则共享 [DefaultIncisionGate] 实例; + * 否则退回进程内单例。 + * + * 由于跨 ClassLoader 类型不能直接传递,所有调用都通过 `IncisionGateApi` 的 + * **基础类型签名** 进行(避免 IncisionGateApi 自身的类装载冲突,proxy 时通过 + * 反射方法名匹配,不需要类型一致)。 + */ +object GateBootstrapper { + + @Volatile + private var bound: IncisionGateApi? = null + + fun current(): IncisionGateApi? = bound + + fun bootstrap(apiVersion: Int): IncisionGateApi { + bound?.let { return it } + synchronized(this) { + bound?.let { return it } + + val inst = InstrumentationBackend.ensure() + val gate = if (inst != null) { + try { + bootstrapViaSystemClassLoader(inst, apiVersion) + } catch (t: Throwable) { + Forensics.warn("通过系统 ClassLoader 启动 gate 失败:${t.javaClass.name}: ${t.message}") + if (Forensics.DEBUG) t.printStackTrace() + bootstrapViaExchanges(apiVersion) + } + } else { + bootstrapViaExchanges(apiVersion) + } + bound = gate + return gate + } + } + + /** + * 把一个最小 gate jar 推入系统 ClassLoader: + * - jar 内仅包含 `taboolib/incision/gate/IncisionGate$V.class`,由 [DefaultIncisionGate] 字节码复制 + * - 通过 `Instrumentation.appendToSystemClassLoaderSearch(jar)` 后 Class.forName 即可 + */ + private fun bootstrapViaSystemClassLoader(inst: Instrumentation, apiVersion: Int): IncisionGateApi { + val gateClassName = "taboolib.incision.gate.IncisionGate\$V$apiVersion" + val internal = gateClassName.replace('.', '/') + val sysCL = ClassLoader.getSystemClassLoader() + + // 已存在则取 delegate + val existing = try { Class.forName(gateClassName, true, sysCL) } catch (_: Throwable) { null } + if (existing != null) { + val delegate = existing.getMethod("getDelegate").invoke(null) + if (delegate != null) return wrapAsApi(delegate, apiVersion) + // delegate 为 null — 被另一个插件创建但尚未 setDelegate;自己设 + val gate = DefaultIncisionGate(apiVersion) + existing.getMethod("setDelegate", Object::class.java).invoke(null, gate) + return gate + } + + // 合成 holder jar 并推入系统 ClassLoader + val jar = File.createTempFile("incision-gate-v$apiVersion-", ".jar").apply { deleteOnExit() } + val manifest = Manifest().apply { + mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0" + } + JarOutputStream(FileOutputStream(jar), manifest).use { out -> + val bytes = SystemGateClassFactory.makeGateClassBytes(internal, apiVersion) + out.putNextEntry(JarEntry("$internal.class")) + out.write(bytes) + out.closeEntry() + } + try { + inst.appendToSystemClassLoaderSearch(java.util.jar.JarFile(jar)) + } catch (t: Throwable) { + throw Trauma.Attach.AgentLoadFailed(jar.absolutePath, t) + } + val cls = Class.forName(gateClassName, true, sysCL) + val gate = DefaultIncisionGate(apiVersion) + cls.getMethod("setDelegate", Object::class.java).invoke(null, gate) + return gate + } + + /** + * fallback 路径:通过 Bukkit Exchanges 共享 gate;若当前平台不可用则退回进程内单例。 + */ + private fun bootstrapViaExchanges(apiVersion: Int): IncisionGateApi { + val key = "incision.gate.v$apiVersion" + val instance = try { + Exchanges.getOrPut(key) { DefaultIncisionGate(apiVersion) } + } catch (t: Throwable) { + Forensics.warn("Exchanges 不可用,使用进程内单例:${t.message}") + DefaultIncisionGate(apiVersion) + } + return wrapAsApi(instance, apiVersion) + } + + /** 把跨 ClassLoader 的对象包成 IncisionGateApi(通过 java.lang.reflect.Proxy) */ + private fun wrapAsApi(instance: Any, apiVersion: Int): IncisionGateApi { + if (instance is IncisionGateApi) return instance + val proxy = java.lang.reflect.Proxy.newProxyInstance( + IncisionGateApi::class.java.classLoader, + arrayOf(IncisionGateApi::class.java), + ) { _, method, args -> + val target = instance.javaClass.methods.firstOrNull { it.name == method.name && it.parameterCount == (args?.size ?: 0) } + ?: throw RuntimeException("跨 ClassLoader gate 无方法 ${method.name}") + try { + target.invoke(instance, *(args ?: emptyArray())) + } catch (e: java.lang.reflect.InvocationTargetException) { + throw e.cause ?: e + } + } + return proxy as IncisionGateApi + } + +} + +/** + * 合成系统级 gate holder 类的字节码。 + * + * 生成的类仅用 JDK 类型,作为系统 ClassLoader 中的"发现锚点": + * - INSTANCE:自身实例(供 IncisionGateLocator 发现) + * - API_VERSION:协议版本常量 + * - delegate:volatile Object,持有真正的 DefaultIncisionGate(位于插件 CL) + * - setDelegate / getDelegate:访问器 + * + * 真正的 gate 逻辑由 DefaultIncisionGate 实现,通过 delegate 字段桥接。 + */ +internal object SystemGateClassFactory { + + fun makeGateClassBytes(internalName: String, apiVersion: Int): ByteArray { + val cw = org.objectweb.asm.ClassWriter(org.objectweb.asm.ClassWriter.COMPUTE_FRAMES or org.objectweb.asm.ClassWriter.COMPUTE_MAXS) + cw.visit(V1_8, ACC_PUBLIC or ACC_FINAL, internalName, null, "java/lang/Object", emptyArray()) + + cw.visitField(ACC_PUBLIC or ACC_STATIC or ACC_FINAL, "INSTANCE", "L$internalName;", null, null).visitEnd() + cw.visitField(ACC_PUBLIC or ACC_STATIC or ACC_FINAL, "API_VERSION", "I", null, apiVersion).visitEnd() + cw.visitField(ACC_PRIVATE or ACC_STATIC or ACC_VOLATILE, "delegate", "Ljava/lang/Object;", null, null).visitEnd() + + // private () + var mv = cw.visitMethod(ACC_PRIVATE, "", "()V", null, null) + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false) + mv.visitInsn(RETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + + // : INSTANCE = new Self() + mv = cw.visitMethod(ACC_STATIC, "", "()V", null, null) + mv.visitCode() + mv.visitTypeInsn(NEW, internalName) + mv.visitInsn(DUP) + mv.visitMethodInsn(INVOKESPECIAL, internalName, "", "()V", false) + mv.visitFieldInsn(PUTSTATIC, internalName, "INSTANCE", "L$internalName;") + mv.visitInsn(RETURN) + mv.visitMaxs(2, 0) + mv.visitEnd() + + // public static void setDelegate(Object d) + mv = cw.visitMethod(ACC_PUBLIC or ACC_STATIC, "setDelegate", "(Ljava/lang/Object;)V", null, null) + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitFieldInsn(PUTSTATIC, internalName, "delegate", "Ljava/lang/Object;") + mv.visitInsn(RETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + + // public static Object getDelegate() + mv = cw.visitMethod(ACC_PUBLIC or ACC_STATIC, "getDelegate", "()Ljava/lang/Object;", null, null) + mv.visitCode() + mv.visitFieldInsn(GETSTATIC, internalName, "delegate", "Ljava/lang/Object;") + mv.visitInsn(ARETURN) + mv.visitMaxs(1, 0) + mv.visitEnd() + cw.visitEnd() + return cw.toByteArray() + } + + private const val V1_8 = org.objectweb.asm.Opcodes.V1_8 + private const val ACC_PUBLIC = org.objectweb.asm.Opcodes.ACC_PUBLIC + private const val ACC_PRIVATE = org.objectweb.asm.Opcodes.ACC_PRIVATE + private const val ACC_STATIC = org.objectweb.asm.Opcodes.ACC_STATIC + private const val ACC_FINAL = org.objectweb.asm.Opcodes.ACC_FINAL + private const val ACC_VOLATILE = org.objectweb.asm.Opcodes.ACC_VOLATILE + private const val ALOAD = org.objectweb.asm.Opcodes.ALOAD + private const val RETURN = org.objectweb.asm.Opcodes.RETURN + private const val ARETURN = org.objectweb.asm.Opcodes.ARETURN + private const val DUP = org.objectweb.asm.Opcodes.DUP + private const val NEW = org.objectweb.asm.Opcodes.NEW + private const val INVOKESPECIAL = org.objectweb.asm.Opcodes.INVOKESPECIAL + private const val PUTSTATIC = org.objectweb.asm.Opcodes.PUTSTATIC + private const val GETSTATIC = org.objectweb.asm.Opcodes.GETSTATIC +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateApi.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateApi.kt new file mode 100644 index 000000000..aff648d3a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateApi.kt @@ -0,0 +1,61 @@ +package taboolib.module.incision.gate + +/** + * IncisionGate 的跨 ClassLoader 契约。 + * + * 约定:**只用基础类型与 proxy 接口**,不允许出现插件 ClassLoader 中才存在的类型。 + * 真正的实现类由 GateBootstrapper 合成并通过 `Instrumentation.appendToSystemClassLoaderSearch` + * 推入系统 ClassLoader;若 Instrumentation 不可用,则通过 Bukkit ServicesManager (Exchanges) + * 共享实例,并由调用方以 invocation handler 代理。 + */ +interface IncisionGateApi { + + /** 确保目标方法已物理织入 dispatcher 调用。返回是否是本次新织入。 */ + fun ensureImplanted(targetSignature: String, weaver: BytecodeWeaverProxy): Boolean + + /** 注册一条 advice。返回 token。 */ + fun register(targetSignature: String, advice: AdviceProxy): GlobalSutureToken + + /** 卸载一条 advice。 */ + fun unregister(token: GlobalSutureToken): Boolean + + /** 目标方法被触发时,由字节码调用进来。 */ + fun dispatch(targetSignature: String, self: Any?, args: Array): Any? + + /** 当前网关协议版本 */ + fun apiVersion(): Int + + /** 支持的 advice 种类(字符串化) */ + fun supportedAdviceTypes(): Set + + /** 按插件名列出已注册的 advice */ + fun listByPlugin(pluginName: String): List + + /** 强制卸载某个 ClassLoader 下的全部 advice(用于插件卸载) */ + fun healByClassLoader(cl: ClassLoader): Int +} + +/** 全局 token — 跨 ClassLoader 反射调用 unregister 时作为句柄 */ +interface GlobalSutureToken { + val targetSignature: String + val incisionId: String + val pluginName: String +} + +/** 跨 ClassLoader 桥 — 插件的 weaver 通过该接口代理被调用 */ +interface BytecodeWeaverProxy { + /** 输入类 internalName 与原字节码,返回织入后的字节码(或 null 表示不修改) */ + fun transform(internalName: String, bytes: ByteArray): ByteArray? + + fun ownerClass(): String +} + +/** 跨 ClassLoader advice 句柄 */ +interface AdviceProxy { + val incisionId: String + val kind: String // "LEAD" / "TRAIL" / "SPLICE" / ... + val priority: Int + val pluginName: String + fun classLoader(): ClassLoader? + fun invoke(targetSignature: String, self: Any?, args: Array): Any? +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateLocator.kt b/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateLocator.kt new file mode 100644 index 000000000..290537d6a --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/gate/IncisionGateLocator.kt @@ -0,0 +1,89 @@ +package taboolib.module.incision.gate + +import taboolib.module.incision.diagnostic.Forensics + +/** + * Gate 定位器 — 负责"接入现有网关 + 版本协商"。 + * + * 系统 ClassLoader 上的 holder 类 `taboolib.incision.gate.IncisionGate$V` 仅作为发现锚点, + * 真正的 gate 实现(DefaultIncisionGate)存储在 holder 的 `delegate` 字段中。 + * + * 流程(由 [IncisionBootstrap] 在 ENABLE 阶段调用): + * 1. 查系统 ClassLoader 上是否存在 holder 类 + * 2. 不存在 → 通过 [GateBootstrapper] 自己创建 + * 3. 存在 → 从 holder 取 delegate,通过 Proxy 桥接跨 CL 调用 + */ +object IncisionGateLocator { + + /** 查找并接入网关;若 JVM 上不存在则自建 */ + fun locateOrCreate(apiVersion: Int): IncisionGateApi { + val existing = findExistingGate(apiVersion) + if (existing != null) { + Forensics.info("Gate located: apiVersion=$apiVersion reused=${existing.javaClass.name}") + return existing + } + val created = GateBootstrapper.bootstrap(apiVersion) + Forensics.info("Gate created: apiVersion=${created.apiVersion()}") + return created + } + + /** 扫描系统 ClassLoader 寻找已有网关实例(任何 API 版本) */ + fun listGates(): List { + val sys = ClassLoader.getSystemClassLoader() + val out = mutableListOf() + for (v in 1..MAX_SUPPORTED_VERSION) { + val delegate = getDelegateFromHolder(sys, v) ?: continue + out += wrap(delegate, v) ?: continue + } + return out + } + + private fun findExistingGate(apiVersion: Int): IncisionGateApi? { + val sys = ClassLoader.getSystemClassLoader() + // 优先查相同 API 版本 + val delegate = getDelegateFromHolder(sys, apiVersion) + if (delegate != null) return wrap(delegate, apiVersion) + + // 再查更高版本(向前兼容) + for (v in (apiVersion + 1)..MAX_SUPPORTED_VERSION) { + val higher = getDelegateFromHolder(sys, v) + if (higher != null) { + Forensics.warn("Gate apiVersion=$v 已存在;本插件 api=$apiVersion 接入兼容子集") + return wrap(higher, v) + } + } + return null + } + + /** 从系统 CL 的 holder 类中取 delegate 对象 */ + private fun getDelegateFromHolder(sys: ClassLoader, apiVersion: Int): Any? { + val cls = try { + Class.forName("taboolib.incision.gate.IncisionGate\$V$apiVersion", false, sys) + } catch (_: Throwable) { return null } + return try { + cls.getMethod("getDelegate").invoke(null) + } catch (_: Throwable) { null } + } + + private fun wrap(instance: Any, apiVersion: Int): IncisionGateApi? { + if (instance is IncisionGateApi) return instance + return try { + java.lang.reflect.Proxy.newProxyInstance( + IncisionGateApi::class.java.classLoader, + arrayOf(IncisionGateApi::class.java), + ) { _, method, args -> + val target = instance.javaClass.methods.firstOrNull { it.name == method.name && it.parameterCount == (args?.size ?: 0) } + ?: throw RuntimeException("gate 缺少方法 ${method.name}") + try { + target.invoke(instance, *(args ?: emptyArray())) + } catch (e: java.lang.reflect.InvocationTargetException) { + throw e.cause ?: e + } + } as IncisionGateApi + } catch (t: Throwable) { + Forensics.error("wrap gate 失败", t); null + } + } + + private const val MAX_SUPPORTED_VERSION = 9 +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/lifecycle/AutoHealHandler.kt b/module/incision/src/main/kotlin/taboolib/module/incision/lifecycle/AutoHealHandler.kt new file mode 100644 index 000000000..fefcd366f --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/lifecycle/AutoHealHandler.kt @@ -0,0 +1,50 @@ +package taboolib.module.incision.lifecycle + +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher + +/** + * 自动卸载 — 当某 ClassLoader 被释放或某插件 disable 时,从 dispatcher 链中移除 + * 该 ClassLoader 拥有的全部 advice。 + * + * 接入方式: + * - 调用方在自己的 PluginDisableEvent 监听器里调用 [healByPlugin] + * - 或在 Awake DISABLE 阶段调用 [healByClassLoader] 传入本插件 ClassLoader + * + * incision 不直接订阅 Bukkit 事件,避免对 platform 模块的硬依赖。 + */ +object AutoHealHandler { + + /** 卸载某 ClassLoader 上注册的全部 advice,返回卸载条目数。 */ + fun healByClassLoader(cl: ClassLoader): Int { + var n = 0 + // 复制以避免并发改动 + val sutures = SurgeryRegistry.list().toList() + for (s in sutures) { + val targets = s.targets.toList() + var anyMatched = false + for (t in targets) { + val chain = TheatreDispatcher.chainOf(t) + val before = chain.list().size + val removed = chain.removeByClassLoader(cl) + n += removed + if (removed > 0 && removed == before) anyMatched = true + } + if (anyMatched) { + s.heal() + } + } + if (n > 0) Forensics.info("AutoHeal: classLoader=${cl} removed=$n advice") + return n + } + + /** 按插件名前缀卸载(id 以 plugin 包名为前缀)。 */ + fun healByPlugin(pluginPackagePrefix: String): Int { + var n = 0 + val sutures = SurgeryRegistry.list().filter { it.id.startsWith(pluginPackagePrefix) }.toList() + for (s in sutures) if (s.heal()) n++ + if (n > 0) Forensics.info("AutoHeal: pluginPrefix=$pluginPackagePrefix removed=$n suture") + return n + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/Backend.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/Backend.kt new file mode 100644 index 000000000..316002adc --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/Backend.kt @@ -0,0 +1,26 @@ +package taboolib.module.incision.loader + +/** + * 织入后端 — 负责让 weaver 的字节码生效。 + * + * 三种后端: + * - [InstrumentationBackend] 主力:通过 java.lang.instrument 跨 ClassLoader retransform + * - [ClassLoaderHookBackend] 兜底:拦截 PluginClassLoader.findClass(仅命中后续加载) + * - [PipelineBackend] NMSProxy 兼容:接入 TabooLib RemapTranslation 的额外 transformer 链 + */ +interface Backend { + + val name: String + + fun available(): Boolean + + /** 为指定类名注册 transformer;返回可用于取消的 token */ + fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): BackendToken + + /** 立即触发一次 retransform(若支持) */ + fun retransform(className: String): Boolean = false + + interface BackendToken { + fun remove() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/ClassLoaderHookBackend.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/ClassLoaderHookBackend.kt new file mode 100644 index 000000000..cc60937bd --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/ClassLoaderHookBackend.kt @@ -0,0 +1,47 @@ +package taboolib.module.incision.loader + +import taboolib.module.incision.diagnostic.Forensics +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap + +/** + * Fallback 后端 — 反射劫持 PluginClassLoader(或当前线程 contextClassLoader)的 findClass/defineClass。 + * + * 局限:仅命中 hook 注册之后才被加载的类,对已加载类无效。适合启动早期注册。 + */ +object ClassLoaderHookBackend : Backend { + + private val transformers = ConcurrentHashMap ByteArray?>>() + + @Volatile + private var installed = false + + override val name: String = "ClassLoaderHook" + + override fun available(): Boolean = true + + override fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): Backend.BackendToken { + val key = className.replace('.', '/') + transformers.computeIfAbsent(key) { mutableListOf() }.add(transformer) + ensureInstalled() + return object : Backend.BackendToken { + override fun remove() { transformers[key]?.remove(transformer) } + } + } + + @Synchronized + private fun ensureInstalled() { + if (installed) return + installed = true + // 当前 ClassLoader 链上 walk,注入 transform 检查点(通过子类化 ClassLoader 不可行; + // 这里只能依赖 hook 在 defineClass 之前 — 通过 Java Agent ClassFileTransformer 才能真正生效) + // 因此该 backend 实际是个"标记",转译动作仍要靠 InstrumentationBackend; + // 当 InstrumentationBackend 可用时,这里把 transformers 委派过去。 + for ((cls, list) in transformers) { + for (t in list) { + InstrumentationBackend.addTransformer(cls, t) + } + } + Forensics.info("ClassLoaderHookBackend: 已委派 ${transformers.size} 个 transformer 给 InstrumentationBackend") + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/InstrumentationBackend.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/InstrumentationBackend.kt new file mode 100644 index 000000000..fe1580216 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/InstrumentationBackend.kt @@ -0,0 +1,122 @@ +package taboolib.module.incision.loader + +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.attach.ManualSelfAttach +import taboolib.module.incision.loader.attach.ByteBuddyAttacher +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.security.ProtectionDomain +import java.util.concurrent.ConcurrentHashMap + +/** + * Instrumentation 后端 — 主力路径。 + * + * 启动时尝试 self-attach 拿到 Instrumentation: + * 1. 优先 ByteBuddyAttacher(如果 byte-buddy-agent 在 classpath) + * 2. 兜底 ManualSelfAttach(JDK 8 走 tools.jar;JDK 9+ 走 jdk.attach 模块) + * + * 拿到后注册一个 ClassFileTransformer,按 className 路由到具体 transformer。 + */ +object InstrumentationBackend : Backend { + + @Volatile + private var inst: Instrumentation? = null + + @Volatile + private var resolveFailed = false + + private val transformers = ConcurrentHashMap ByteArray?>>() + + override val name: String = "Instrumentation" + + override fun available(): Boolean = ensure() != null + + fun ensure(): Instrumentation? { + inst?.let { return it } + if (resolveFailed) return null + synchronized(this) { + inst?.let { return it } + if (resolveFailed) return null + val resolved = resolve() + if (resolved == null) { + resolveFailed = true + return null + } + inst = resolved + register(resolved) + return resolved + } + } + + private fun resolve(): Instrumentation? { + // self-attach 在大多数 Minecraft 服务端(Paper / Spigot)被默认禁用,是预期路径。 + // 只在 -Dtaboolib.incision.debug=true 时打印;否则静默回退到 ASM-only 路径。 + return try { + ByteBuddyAttacher.tryInstall() + } catch (t: Throwable) { + Forensics.debug("ByteBuddyAttacher 不可用: ${t.message}") + null + } ?: try { + ManualSelfAttach.attach() + } catch (t: Trauma.Attach) { + Forensics.debug("ManualSelfAttach 不可用: ${t.message}") + null + } catch (t: Throwable) { + Forensics.debug("ManualSelfAttach 不可用: ${t.message}") + null + } + } + + private fun register(i: Instrumentation) { + i.addTransformer(object : ClassFileTransformer { + override fun transform( + loader: ClassLoader?, className: String?, + classBeingRedefined: Class<*>?, protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray, + ): ByteArray? { + if (className == null) return null + val list = transformers[className] ?: return null + var bytes = classfileBuffer + val prev = taboolib.module.incision.weaver.Scalpel.currentTransformLoader.get() + taboolib.module.incision.weaver.Scalpel.currentTransformLoader.set(loader) + try { + for (t in list) { + val out = try { t(bytes) } catch (e: Throwable) { + Forensics.error("transformer 执行失败: $className", e); null + } + if (out != null) bytes = out + } + } finally { + taboolib.module.incision.weaver.Scalpel.currentTransformLoader.set(prev) + } + return if (bytes === classfileBuffer) null else bytes + } + }, true) + } + + override fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): Backend.BackendToken { + val key = className.replace('.', '/') + transformers.computeIfAbsent(key) { mutableListOf() }.add(transformer) + return object : Backend.BackendToken { + override fun remove() { transformers[key]?.remove(transformer) } + } + } + + override fun retransform(className: String): Boolean { + val i = ensure() ?: return false + val cls = findClass(className) ?: return false + return try { i.retransformClasses(cls); true } catch (e: Throwable) { + Forensics.warn("retransform 失败: $className — ${e.message}"); false + } + } + + private fun findClass(className: String): Class<*>? { + // 1. context classloader + try { return Class.forName(className, false, Thread.currentThread().contextClassLoader) } catch (_: Throwable) {} + // 2. all loaded classes via Instrumentation + val i = inst ?: return null + val internalName = className.replace('.', '/') + return i.allLoadedClasses.firstOrNull { it.name == className || it.name.replace('.', '/') == internalName } + } +} 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 new file mode 100644 index 000000000..4c3069dec --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/JvmtiBackend.kt @@ -0,0 +1,298 @@ +package taboolib.module.incision.loader + +import taboolib.module.incision.diagnostic.Forensics +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +/** + * JVMTI native backend — retransforms already-loaded classes without + * -javaagent or -XX:+EnableDynamicAgentLoading. + * + * Loads a platform-specific native library bundled in the jar under + * native/{windows|linux|macos}/{x64|arm64}/incision-jvmti.{dll|so|dylib} + * + * The native lib uses JNI_OnLoad + system property "incision.jvmti.class" + * for dynamic registration, so Gradle Shadow relocation works transparently. + * + * The native lib calls back into [onClassFileLoad] for every class that + * is (re)transformed, routing bytes through the registered transformers. + */ +object JvmtiBackend : Backend { + + private val transformers = ConcurrentHashMap ByteArray?>>() + + @Volatile private var loaded = false + @Volatile private var available = false + + override val name: String = "JVMTI" + + override fun available(): Boolean { + if (!loaded) tryLoad() + return available + } + + override fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): Backend.BackendToken { + val key = className.replace('.', '/') + transformers.computeIfAbsent(key) { mutableListOf() }.add(transformer) + return object : Backend.BackendToken { + override fun remove() { transformers[key]?.remove(transformer) } + } + } + + override fun retransform(className: String): Boolean { + if (!available()) return false + val cls = findLoadedClass(className) + if (cls == null) { + Forensics.debug("JvmtiBackend.retransform: $className not loaded yet, will transform on load") + return true + } + return try { + val ok = nRetransform(cls) + Forensics.debug("JvmtiBackend.retransform $className → $ok (loader=${cls.classLoader})") + ok + } catch (t: Throwable) { + Forensics.warn("JvmtiBackend.retransform failed: $className — ${t.message}") + false + } + } + + /** Called from native ClassFileLoadHook for every (re)loaded class. */ + @JvmStatic + fun onClassFileLoad(loader: ClassLoader?, name: String, bytes: ByteArray): ByteArray? { + 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 + } + if (out != null) { cur = out; changed = true } + } + Forensics.debug("JvmtiBackend.onClassFileLoad: $name → changed=$changed (${cur.size} bytes)") + return if (changed) cur else null + } + + // ---------------------------------------------------------------- + // Native methods — registered dynamically via JNI_OnLoad + // ---------------------------------------------------------------- + + @JvmStatic private external fun nInit(backendClass: Class<*>): Boolean + @JvmStatic private external fun nRetransform(target: Class<*>): Boolean + @JvmStatic private external fun nAvailable(): Boolean + @JvmStatic external fun nDispose() + @JvmStatic external fun nDefineClass(loader: ClassLoader?, name: String, bytes: ByteArray): Class<*>? + + @JvmStatic private external fun nCacheOriginal(internalName: String, bytes: ByteArray): Boolean + @JvmStatic private external fun nGetCachedOriginal(internalName: String): ByteArray? + @JvmStatic private external fun nExtractClassBytes(target: Class<*>): ByteArray? + @JvmStatic private external fun nPurgeCache(internalName: String) + + // 通用字段/方法访问器 — JNI 层绕过 Java 访问控制 + @JvmStatic external fun nFieldGet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? + @JvmStatic external fun nFieldSet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) + @JvmStatic external fun nStaticFieldGet(ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? + @JvmStatic external fun nStaticFieldSet(ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) + @JvmStatic external fun nInvokeMethod(obj: Any?, ownerClass: Class<*>, methodName: String, methodDesc: String, args: Array?): Any? + + fun defineClassInClassLoader(loader: ClassLoader?, name: String, bytes: ByteArray): Class<*>? { + if (!available()) return null + return try { + val cls = nDefineClass(loader, name.replace('.', '/'), bytes) + if (cls != null) Forensics.debug("JvmtiBackend.defineClass: $name in ${loader ?: "bootstrap"}") + cls + } catch (t: Throwable) { + Forensics.warn("JvmtiBackend.defineClass failed: $name — ${t.message}") + null + } + } + + fun cacheOriginal(owner: String, bytes: ByteArray): Boolean { + if (!available()) return false + return try { + nCacheOriginal(owner, bytes) + } catch (t: Throwable) { + Forensics.warn("nCacheOriginal 失败: ${t.message}") + false + } + } + + fun getCachedOriginal(owner: String): ByteArray? { + if (!available()) return null + return try { + nGetCachedOriginal(owner) + } catch (t: Throwable) { + Forensics.warn("nGetCachedOriginal 失败: ${t.message}") + null + } + } + + fun extractClassBytes(target: Class<*>): ByteArray? { + if (!available()) return null + return try { + nExtractClassBytes(target) + } catch (t: Throwable) { + Forensics.warn("nExtractClassBytes 失败: ${t.message}") + null + } + } + + fun purgeCache(owner: String) { + if (!available()) return + try { + nPurgeCache(owner) + } catch (t: Throwable) { + Forensics.warn("nPurgeCache 失败: ${t.message}") + } + } + + // ---------------------------------------------------------------- + // 通用访问器高级 API — 绕过 Java 访问控制 + // ---------------------------------------------------------------- + + fun fieldGet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? { + if (!available()) return null + return try { + nFieldGet(obj, ownerClass, fieldName, fieldDesc) + } catch (t: Throwable) { + Forensics.warn("nFieldGet 失败: ${ownerClass.name}.$fieldName — ${t.message}") + null + } + } + + fun fieldSet(obj: Any, ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) { + if (!available()) return + try { + nFieldSet(obj, ownerClass, fieldName, fieldDesc, value) + } catch (t: Throwable) { + Forensics.warn("nFieldSet 失败: ${ownerClass.name}.$fieldName — ${t.message}") + } + } + + fun staticFieldGet(ownerClass: Class<*>, fieldName: String, fieldDesc: String): Any? { + if (!available()) return null + return try { + nStaticFieldGet(ownerClass, fieldName, fieldDesc) + } catch (t: Throwable) { + Forensics.warn("nStaticFieldGet 失败: ${ownerClass.name}.$fieldName — ${t.message}") + null + } + } + + fun staticFieldSet(ownerClass: Class<*>, fieldName: String, fieldDesc: String, value: Any?) { + if (!available()) return + try { + nStaticFieldSet(ownerClass, fieldName, fieldDesc, value) + } catch (t: Throwable) { + Forensics.warn("nStaticFieldSet 失败: ${ownerClass.name}.$fieldName — ${t.message}") + } + } + + fun invokeMethod(obj: Any?, ownerClass: Class<*>, methodName: String, methodDesc: String, args: Array?): Any? { + if (!available()) return null + return try { + nInvokeMethod(obj, ownerClass, methodName, methodDesc, args) + } catch (t: Throwable) { + Forensics.warn("nInvokeMethod 失败: ${ownerClass.name}.$methodName — ${t.message}") + null + } + } + + // ---------------------------------------------------------------- + // Library loading + // ---------------------------------------------------------------- + + @Synchronized + fun tryLoad(): Boolean { + if (loaded) return available + loaded = true + val lib = extractNativeLib() ?: return false + return try { + // Set system property BEFORE System.load so JNI_OnLoad can + // find the (possibly relocated) class and RegisterNatives. + System.setProperty("incision.jvmti.class", JvmtiBackend::class.java.name) + System.load(lib.absolutePath) + available = nInit(JvmtiBackend::class.java) + if (available) Forensics.info("JvmtiBackend: JVMTI native loaded from $lib") + else Forensics.warn("JvmtiBackend: nInit returned false") + available + } catch (t: Throwable) { + Forensics.warn("JvmtiBackend: failed to load native lib — ${t.message}") + false + } + } + + private fun extractNativeLib(): File? { + val (os, prefix, ext) = platformTriple() ?: return null + val arch = archName() + val resource = "native/$os/$arch/${prefix}incision-jvmti.$ext" + val stream = JvmtiBackend::class.java.classLoader + .getResourceAsStream(resource) ?: run { + Forensics.warn("JvmtiBackend: resource not found: $resource") + return null + } + val bytes = stream.use { it.readBytes() } + val hash = bytes.fold(0L) { acc, b -> acc * 31 + b.toLong() }.toString(16).takeLast(8) + val dir = File(System.getProperty("java.io.tmpdir"), "incision-native").also { it.mkdirs() } + val target = File(dir, "${prefix}incision-jvmti-$hash.$ext") + if (target.exists()) { + return target + } + try { + target.outputStream().use { output -> output.write(bytes) } + } catch (e: Throwable) { + Forensics.warn("JvmtiBackend: extract failed — ${e.message}") + if (target.exists()) return target + return null + } + return target + } + + private fun platformTriple(): Triple? = when { + System.getProperty("os.name").lowercase().contains("win") -> Triple("windows", "", "dll") + System.getProperty("os.name").lowercase().contains("mac") -> Triple("macos", "lib", "dylib") + System.getProperty("os.name").lowercase().contains("linux") -> Triple("linux", "lib", "so") + else -> null + } + + private fun archName(): String { + val arch = System.getProperty("os.arch").lowercase() + return when { + arch.contains("aarch64") || arch.contains("arm64") -> "arm64" + else -> "x64" + } + } + + private val findLoadedClassMethod: java.lang.reflect.Method? by lazy { + try { + ClassLoader::class.java.getDeclaredMethod("findLoadedClass", String::class.java) + .also { it.isAccessible = true } + } catch (_: Throwable) { null } + } + + private fun findLoadedClass(className: String): Class<*>? { + val m = findLoadedClassMethod ?: return findClass(className) + for (cl in classLoaders()) { + try { + val cls = m.invoke(cl, className) as? Class<*> + if (cls != null) return cls + } catch (_: Throwable) {} + } + return null + } + + private fun classLoaders(): List = buildList { + Thread.currentThread().contextClassLoader?.let { add(it) } + add(JvmtiBackend::class.java.classLoader) + ClassLoader.getSystemClassLoader()?.let { add(it) } + } + + private fun findClass(className: String): Class<*>? { + return try { Class.forName(className, false, Thread.currentThread().contextClassLoader) } + catch (_: Throwable) { + try { Class.forName(className, false, JvmtiBackend::class.java.classLoader) } + catch (_: Throwable) { null } + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/PipelineBackend.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/PipelineBackend.kt new file mode 100644 index 000000000..4b8ba34c8 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/PipelineBackend.kt @@ -0,0 +1,83 @@ +package taboolib.module.incision.loader + +import taboolib.module.incision.diagnostic.Forensics +import java.util.concurrent.ConcurrentHashMap + +/** + * Pipeline 后端 — 供 TabooLib `RemapTranslation` / `AsmClassTranslation` 在 + * 完成自身 remap/require/dynamic 后调用 incision 的二次织入。 + * + * 目前 TabooLib 并未公开 `extraTransformers` 挂钩点(见 PLAN 末的可选 PR), + * 因此本后端提供以下接入方式: + * + * 1. **推式** — incision 调用方注册 transformer,此后任何调用 [apply] 的代码 + * (未来由 NMSProxy 管线调用)都会按顺序应用所有注册的 transformer。 + * 2. **拉式** — TabooLib 自己在 NMSProxy 最终产物上调用 `PipelineBackend.apply(className, bytes)` + * 即可把 incision 织入塞进去。 + * + * 当 TabooLib 提供 hook 前,本后端也可由 [InstrumentationBackend] 兜底复用: + * NMSProxy 的 impl 类一旦被加载,InstrumentationBackend 也能 retransform 它。 + */ +object PipelineBackend : Backend { + + private val transformers = ConcurrentHashMap ByteArray?>>() + + override val name: String = "Pipeline" + override fun available(): Boolean = true + + override fun addTransformer(className: String, transformer: (ByteArray) -> ByteArray?): Backend.BackendToken { + val key = className.replace('.', '/') + transformers.computeIfAbsent(key) { mutableListOf() }.add(transformer) + return object : Backend.BackendToken { + override fun remove() { transformers[key]?.remove(transformer) } + } + } + + /** 供 TabooLib 管线末端调用 — 给定 className + 当前字节码,返回织入后的字节码。 */ + fun apply(className: String, bytes: ByteArray): ByteArray { + val key = className.replace('.', '/') + val list = transformers[key] ?: return bytes + var cur = bytes + for (t in list) { + val out = try { t(cur) } catch (e: Throwable) { + Forensics.error("PipelineBackend transformer 执行失败: $key", e); null + } + if (out != null) cur = out + } + return cur + } + + fun has(className: String): Boolean = transformers.containsKey(className.replace('.', '/')) + + fun clear() = transformers.clear() + + /** + * 把本后端注册进 TabooLib `RemapTranslation.extraTransformers`, + * 使 NMSProxy 管线最后一环调用 incision 织入。 + * + * `:module:bukkit-nms` 不在 classpath 时静默跳过;多次调用幂等。 + */ + fun installIntoTabooLib(): Boolean { + return try { + val cls = Class.forName("taboolib.module.nms.remap.RemapTranslation") + val list = cls.getDeclaredField("extraTransformers").apply { isAccessible = true }.get(null) + as MutableList<(String, ByteArray) -> ByteArray?> + // 用一个固定身份的 transformer,避免重复挂载 + val sentinel = HOOK + if (list.any { it === sentinel }) return true + list += sentinel + Forensics.info("PipelineBackend installed into TabooLib RemapTranslation.extraTransformers") + true + } catch (_: ClassNotFoundException) { + false + } catch (t: Throwable) { + Forensics.warn("PipelineBackend install failed: ${t.message}") + false + } + } + + /** 固定 transformer 实例 — 路由到 [apply],便于重复挂载检测。 */ + private val HOOK: (String, ByteArray) -> ByteArray? = { className, bytes -> + if (!has(className)) null else apply(className, bytes) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/SurgeonScanner.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/SurgeonScanner.kt new file mode 100644 index 000000000..5e0e374b1 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/SurgeonScanner.kt @@ -0,0 +1,560 @@ +package taboolib.module.incision.loader + +import org.tabooproject.reflex.ClassMethod +import org.tabooproject.reflex.LazyEnum +import org.tabooproject.reflex.ReflexClass +import taboolib.common.Inject +import taboolib.common.LifeCycle +import taboolib.common.inject.ClassVisitor +import taboolib.common.platform.Awake +import taboolib.module.incision.annotation.Bypass +import taboolib.module.incision.annotation.Excise +import taboolib.module.incision.annotation.Graft +import taboolib.module.incision.annotation.InsnPattern +import taboolib.module.incision.annotation.KotlinTarget +import taboolib.module.incision.annotation.Lead +import taboolib.module.incision.annotation.Op +import taboolib.module.incision.annotation.Operation +import taboolib.module.incision.annotation.Site +import taboolib.module.incision.annotation.Splice +import taboolib.module.incision.annotation.Step +import taboolib.module.incision.annotation.Surgeon +import taboolib.module.incision.annotation.Trail +import taboolib.module.incision.annotation.Trim +import taboolib.module.incision.annotation.Version +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.DescriptorCodec +import taboolib.module.incision.api.VersionMatchers + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Theatre +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.dsl.SutureImpl +import taboolib.module.incision.dsl.Scalpel +import taboolib.module.incision.pred.AdviceCtx +import taboolib.module.incision.pred.PredCompiler +import taboolib.module.incision.pred.Predicate +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.InsnStep +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * @Surgeon 扫描器 —— 在 CONST 阶段扫描所有标注 @Surgeon 的 object, + * 并且优先级早于 `ClassVisitorAwake(CONST)`,以便尽量覆盖宿主插件自己的 `@Awake(CONST)` 行为。 + * 把注解方法翻译为 advice 并注册到 TheatreDispatcher,同时触发字节码织入。 + */ +@Inject +@Awake +class SurgeonScanner : ClassVisitor((-1).toByte()) { + + override fun getLifeCycle(): LifeCycle = LifeCycle.CONST + + override fun visitEnd(clazz: ReflexClass) { + if (!clazz.hasAnnotation(Surgeon::class.java)) return + val instance = findInstance(clazz) ?: run { + Forensics.warn("@Surgeon ${clazz.name} 无法获取实例 (要求标注在 object 上)") + return + } + val surgeon = clazz.getAnnotationIfPresent(Surgeon::class.java)!! + val defaultPriority = surgeon.property("priority", 0) + + // 每个方法单独组一个 Suture —— id = clazz.name#operationIdSuffix@i + // aggregate entries for weaver install + 聚合调试 + val aggregateEntries = mutableListOf() + val holderKClass = Class.forName(clazz.name).kotlin + val seenSutureIds = HashSet() + + for ((i, m) in clazz.structure.methods.withIndex()) { + // 版本范围过滤 —— 标 @Version 且当前版本不在区间内的 advice 直接跳过, + // 既不进 dispatcher 也不安装 weaver。 + if (m.isAnnotationPresent(Version::class.java)) { + val ver = m.getAnnotation(Version::class.java) + val start = ver.property("start", "") + val end = ver.property("end", "") + val matcherFqcn = ver.property("matcher", "") + val matcher = VersionMatchers.resolve(matcherFqcn) + if (!matcher.matches(start, end)) { + Forensics.debug("@Surgeon ${clazz.name}#${m.name} 因 @Version([$start, $end]) 跳过 (current=${matcher.current()})") + continue + } + } + val operation = if (m.isAnnotationPresent(Operation::class.java)) m.getAnnotation(Operation::class.java) else null + val methodPriority = operation?.property("priority", defaultPriority) ?: defaultPriority + val methodEnabled = operation?.property("enabled", true) ?: true + val methodIdSuffix = operation?.property("id", "")?.takeIf { it.isNotBlank() } ?: m.name + // Suture id 形如 `owner#suffix@idx`:保留 @idx 确保同类内多方法共用同名 id 时互不覆盖 + val sutureId = "${clazz.name}#$methodIdSuffix@$i" + val built = buildEntries(sutureId, methodPriority, methodEnabled, clazz, instance, m) + if (built.isEmpty()) continue + + for (e in built) TheatreDispatcher.register(e) + aggregateEntries += built + val suture = SutureImpl(sutureId, built.map { it.target }.distinct(), holderKClass, built) + try { + SurgeryRegistry.register(sutureId, suture) + seenSutureIds += sutureId + } catch (t: Trauma.Declaration.DuplicateId) { + Forensics.warn("@Surgeon 重复注册被跳过: $sutureId") + } + } + if (aggregateEntries.isEmpty()) return + // weaver 按类粒度一次性安装即可(内部按 owner 分组织入) + Scalpel.installWeaver(aggregateEntries) + Forensics.debug("Surgeon 扫描: ${clazz.name} → ${seenSutureIds.size} suture / ${aggregateEntries.size} advice") + } + + private fun buildEntries( + id: String, + priority: Int, + enabled: Boolean, + owner: ReflexClass, + instance: Any, + m: ClassMethod, + ): List { + val declaration = parseAdviceDeclaration(m) ?: return emptyList() + val parsed = DescriptorCodec.parseMethod(declaration.rawDescriptor) ?: run { + Forensics.warn("@Surgeon 描述符无法解析: ${declaration.rawDescriptor} (来源=${owner.name}#${m.name})") + return emptyList() + } + val aliases = expandTargets(parsed.toCoordinate(), m) + val handler: (Theatre) -> Any? = buildHandler(owner, instance, m) + val classLoader = Class.forName(owner.name).classLoader + val cl = java.lang.ref.WeakReference(classLoader) + // 若 InsnPattern steps 非空,把 OpcodeSeq 注入 siteSpec.pattern(recording 路径消费)。 + val seqPattern = if (declaration.patternDeclared) SitePattern.OpcodeSeq(declaration.insnSteps) else null + // 对于 scope-based advice(Lead/Trail/Splice/Excise),原本没有 siteSpec; + // 带 @InsnPattern 时补一个 Anchor.HEAD + pattern 的占位 siteSpec 让 SiteWeaver 走 recording 路径。 + val siteSpecWithPattern = when { + declaration.siteSpec != null && seqPattern != null -> declaration.siteSpec.copy(pattern = seqPattern) + declaration.siteSpec != null -> declaration.siteSpec + seqPattern != null -> SiteSpec( + anchor = Anchor.HEAD, + kind = declaration.kind, + target = MethodCoordinate("", "", ""), + pattern = seqPattern, + ) + else -> null + } + // 编译 `where` 字符串谓词(空串或编译失败都视为无)。 + val compiledPred: Predicate? = declaration.where.takeIf { it.isNotBlank() }?.let { src -> + try { + PredCompiler.compile(src, AdviceCtx(adviceId = id, classLoader = classLoader)) + } catch (t: Throwable) { + Forensics.warn("@Surgeon where 谓词编译失败: $src (${owner.name}#${m.name}) - ${t.message}") + null + } + } + return aliases.mapIndexed { index, target -> + val entryId = if (aliases.size == 1) id else "$id#$index" + AdviceEntry( + id = entryId, + kind = declaration.kind, + target = target, + priority = priority, + handler = handler, + classLoader = cl, + explicitResumeRequired = declaration.explicitResumeRequired, + sourceKind = "annotation", + aliasGroup = id, + siteSpec = siteSpecWithPattern?.copy(target = target, adviceId = entryId), + compiledPredicate = compiledPred, + predicateSource = declaration.where.ifBlank { null }, + enabled = enabled, + onThrow = declaration.onThrow, + ) + } + } + + private fun parseAdviceDeclaration(m: ClassMethod): AdviceDeclaration? { + if (m.isAnnotationPresent(Lead::class.java)) { + val ann = m.getAnnotation(Lead::class.java) + val scope = ann.property("scope", "") + if (scope.isBlank()) return null + return AdviceDeclaration( + kind = AdviceKind.LEAD, + rawDescriptor = extractMethodDescriptor(scope), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + if (m.isAnnotationPresent(Trail::class.java)) { + val ann = m.getAnnotation(Trail::class.java) + val scope = ann.property("scope", "") + if (scope.isBlank()) return null + return AdviceDeclaration( + kind = AdviceKind.TRAIL, + rawDescriptor = extractMethodDescriptor(scope), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + onThrow = ann.property("onThrow", true), + ) + } + if (m.isAnnotationPresent(Splice::class.java)) { + val ann = m.getAnnotation(Splice::class.java) + val scope = ann.property("scope", "") + if (scope.isBlank()) return null + return AdviceDeclaration( + kind = AdviceKind.SPLICE, + rawDescriptor = extractMethodDescriptor(scope), + explicitResumeRequired = true, + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + if (m.isAnnotationPresent(Bypass::class.java)) { + val ann = m.getAnnotation(Bypass::class.java) + val method = ann.property("method", "") + if (method.isBlank()) return null + val site = toSiteAnnotation(ann.properties()["site"], Site(Anchor.HEAD)) + return AdviceDeclaration( + kind = AdviceKind.BYPASS, + rawDescriptor = method, + siteSpec = if (isWholeMethodBypass(site, ann.properties()["pattern"])) null else toSiteSpec(site, AdviceKind.BYPASS), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + if (m.isAnnotationPresent(Excise::class.java)) { + val ann = m.getAnnotation(Excise::class.java) + val scope = ann.property("scope", "") + if (scope.isBlank()) return null + return AdviceDeclaration( + kind = AdviceKind.EXCISE, + rawDescriptor = extractMethodDescriptor(scope), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + if (m.isAnnotationPresent(Graft::class.java)) { + val ann = m.getAnnotation(Graft::class.java) + val method = ann.property("method", "") + if (method.isBlank()) return null + val site = toSiteAnnotation(ann.properties()["site"], Site(Anchor.HEAD)) + return AdviceDeclaration( + kind = AdviceKind.GRAFT, + rawDescriptor = method, + siteSpec = toSiteSpec(site, AdviceKind.GRAFT), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + if (m.isAnnotationPresent(Trim::class.java)) { + val ann = m.getAnnotation(Trim::class.java) + val method = ann.property("method", "") + if (method.isBlank()) return null + val trimKind = runCatching { Trim.Kind.valueOf(ann.enumName("kind", Trim.Kind.RETURN.name)) } + .getOrDefault(Trim.Kind.RETURN) + // TRIM 默认锚点按 kind 决定:RETURN → Anchor.RETURN(每个 IRETURN/.../ARETURN 之前栈顶就是返回值); + // ARG / VAR → Anchor.HEAD(从方法头部开始读 LV slot 替换)。否则 HEAD 上 TRIM RETURN 会在空栈 + // 上 DUP,立刻 "Cannot pop operand off an empty stack"。 + val defaultAnchor = if (trimKind == Trim.Kind.RETURN) Anchor.RETURN else Anchor.HEAD + val site = toSiteAnnotation(ann.properties()["site"], Site(defaultAnchor)) + val trimIndex = ann.property("index", 0) + val argDesc = if (trimKind == Trim.Kind.ARG) extractArgDescriptor(method, trimIndex) else "" + val baseSpec = toSiteSpec(site, AdviceKind.TRIM) + val retDesc = if (trimKind == Trim.Kind.RETURN) deriveTrimReturnDesc(baseSpec) else "" + return AdviceDeclaration( + kind = AdviceKind.TRIM, + rawDescriptor = method, + siteSpec = baseSpec.copy( + trimKind = trimKind, + trimIndex = trimIndex, + trimArgDescriptor = argDesc, + trimReturnDescriptor = retDesc, + ), + insnSteps = readInsnSteps(ann.properties()["pattern"]), + patternDeclared = hasInsnPattern(ann.properties()["pattern"]), + where = ann.property("where", ""), + ) + } + return null + } + + /** + * 静态推断 TRIM RETURN 模式下栈顶值的 JVM 描述符: + * - INVOKE 锚点:取 [SiteSpec.descPattern] 返回段("...)X" 中的 X) + * - FIELD_GET 锚点:[SiteSpec.descPattern] 即字段类型,直接用 + * - 其它(含 RETURN / HEAD / TAIL):返回空,由 SiteWeaver.applyPlan 用宿主方法返回类型补齐 + */ + private fun deriveTrimReturnDesc(spec: SiteSpec): String { + return when (spec.anchor) { + Anchor.INVOKE -> { + val desc = spec.descPattern + val close = desc.indexOf(')') + if (close in 0 until desc.length - 1) desc.substring(close + 1) else "" + } + Anchor.FIELD_GET -> spec.descPattern + else -> "" + } + } + + /** + * 从 `owner#name(argTypes)retType` 中提取第 [index] 个参数的 JVM 描述符。 + * 解析失败时返回空串(emitter 会保守跳过)。 + */ + private fun extractArgDescriptor(rawMethod: String, index: Int): String { + val parsed = DescriptorCodec.parseMethod(rawMethod) ?: return "" + val desc = parsed.descriptor + val open = desc.indexOf('(') + val close = desc.indexOf(')') + if (open < 0 || close < 0 || close <= open) return "" + val argsDesc = desc.substring(open + 1, close) + val args = splitJvmArgDescriptors(argsDesc) + return args.getOrNull(index) ?: "" + } + + /** 把 `Ljava/lang/String;ILjava/util/List;[I` 拆成 [`Ljava/lang/String;`, `I`, `Ljava/util/List;`, `[I`]。 */ + private fun splitJvmArgDescriptors(args: String): List { + val out = mutableListOf() + var i = 0 + while (i < args.length) { + val start = i + while (i < args.length && args[i] == '[') i++ + when (val c = args.getOrNull(i)) { + 'L' -> { + val semi = args.indexOf(';', i) + if (semi < 0) return out + out += args.substring(start, semi + 1) + i = semi + 1 + } + 'V', 'Z', 'B', 'S', 'I', 'J', 'F', 'D', 'C' -> { + out += args.substring(start, i + 1) + i++ + } + null -> return out + else -> { + out += c.toString() + i++ + } + } + } + return out + } + + /** + * 把 @InsnPattern(注解或运行时 Map 形式)读成 [InsnStep] 列表。 + * 兼容两种 Reflex 返回形态: + * - 原生注解 [InsnPattern](kotlin-reflect 直接取到) + * - `Map` 包含 `steps` 键(Reflex 展开形态) + */ + private fun readInsnSteps(raw: Any?): List { + val stepsArr: Any? = when (raw) { + null -> return emptyList() + is InsnPattern -> raw.steps + is Map<*, *> -> raw["steps"] + else -> return emptyList() + } ?: return emptyList() + val list = mutableListOf() + when (stepsArr) { + is Array<*> -> stepsArr.forEach { s -> readOneStep(s)?.let(list::add) } + is Iterable<*> -> stepsArr.forEach { s -> readOneStep(s)?.let(list::add) } + else -> Unit + } + return list + } + + private fun hasInsnPattern(raw: Any?): Boolean = when (raw) { + null -> false + is InsnPattern -> true + is Map<*, *> -> raw.containsKey("steps") + else -> false + } + + private fun readOneStep(raw: Any?): InsnStep? { + return when (raw) { + null -> null + is Step -> InsnStep( + opcode = raw.opcode.opcode, + ownerFilter = raw.owner, + nameFilter = raw.name, + descFilter = raw.desc, + cstFilter = raw.cst, + repeat = raw.repeat, + ) + is Map<*, *> -> { + val opName = enumNameOf(raw["opcode"], Op.ANY.name) + val op = runCatching { Op.valueOf(opName) }.getOrDefault(Op.ANY) + InsnStep( + opcode = op.opcode, + ownerFilter = raw["owner"]?.toString() ?: "", + nameFilter = raw["name"]?.toString() ?: "", + descFilter = raw["desc"]?.toString() ?: "", + cstFilter = raw["cst"]?.toString() ?: "", + repeat = (raw["repeat"] as? Number)?.toInt() ?: 1, + ) + } + else -> null + } + } + + private fun toSiteSpec(site: Site, kind: AdviceKind): SiteSpec { + val rawTarget = site.target.takeIf { it.isNotBlank() } ?: "" + // 先走 SitePattern 统一解析;再降级转 SiteSpec 三字段以兼容旧 SiteWeaver + val pattern = taboolib.module.incision.weaver.site.pattern.parser.ParserRegistry.DEFAULT + .parse(site.anchor, rawTarget) + var ownerPattern = "" + var namePattern = "" + var descPattern = "" + when (pattern) { + is taboolib.module.incision.weaver.site.pattern.SitePattern.MethodCall -> { + ownerPattern = pattern.owner + namePattern = pattern.name + descPattern = pattern.desc + } + is taboolib.module.incision.weaver.site.pattern.SitePattern.FieldAccess -> { + ownerPattern = pattern.owner + namePattern = pattern.name + descPattern = pattern.desc + } + is taboolib.module.incision.weaver.site.pattern.SitePattern.TypeAlloc -> { + ownerPattern = pattern.internalName + } + is taboolib.module.incision.weaver.site.pattern.SitePattern.Anywhere -> { + if (rawTarget.isNotEmpty()) { + Forensics.debug("@Site ${site.anchor} 忽略 target=$rawTarget") + } + } + is taboolib.module.incision.weaver.site.pattern.SitePattern.OpcodeSeq -> { + // OpcodeSeq 由方法级 @InsnPattern 驱动,不从 target 产生,这里不会命中 + } + null -> { + Forensics.warn("@Site ${site.anchor} target 解析失败: $rawTarget") + } + } + return SiteSpec( + anchor = site.anchor, + ownerPattern = ownerPattern, + namePattern = namePattern, + descPattern = descPattern, + shift = site.shift, + ordinal = site.ordinal, + kind = kind, + target = MethodCoordinate("", "", ""), + offset = site.offset, + ) + } + + private fun toSiteAnnotation(raw: Any?, fallback: Site): Site { + return when (raw) { + null -> fallback + is Site -> raw + is Map<*, *> -> { + val anchorName = enumNameOf(raw["anchor"], fallback.anchor.name) + val shiftName = enumNameOf(raw["shift"], fallback.shift.name) + val ordinal = (raw["ordinal"] as? Number)?.toInt() ?: fallback.ordinal + val offset = (raw["offset"] as? Number)?.toInt() ?: fallback.offset + Site( + anchor = runCatching { Anchor.valueOf(anchorName) }.getOrDefault(fallback.anchor), + target = raw["target"]?.toString() ?: fallback.target, + shift = runCatching { taboolib.module.incision.api.Shift.valueOf(shiftName) }.getOrDefault(fallback.shift), + ordinal = ordinal, + offset = offset, + ) + } + else -> fallback + } + } + + /** Reflex 的 enum 注解值是 [LazyEnum](toString 不返回 enum name)。统一抽 name;非 enum 类型走 toString。 */ + private fun enumNameOf(raw: Any?, default: String): String = when (raw) { + null -> default + is LazyEnum -> raw.name + else -> raw.toString() + } + + private fun expandTargets(primary: MethodCoordinate, m: ClassMethod): List { + val out = linkedSetOf(primary) + if (!m.isAnnotationPresent(KotlinTarget::class.java)) return out.toList() + val kt = m.getAnnotation(KotlinTarget::class.java) + val companionInstance = kt.property("companionInstance", false) + val jvmStaticBridge = kt.property("jvmStaticBridge", false) + if (companionInstance && !primary.owner.endsWith("$" + "Companion")) { + out += MethodCoordinate(primary.owner + "$" + "Companion", primary.name, primary.descriptor) + } + if (jvmStaticBridge) { + // Kotlin 编译器对 @JvmStatic 方法有两条调用路径: + // 1. 外部类的 static bridge(Java 调用者使用) + // 2. $Companion 的 instance method(Kotlin 调用者使用) + // 必须同时织入两者 + if (primary.owner.endsWith("$" + "Companion")) { + out += MethodCoordinate(primary.owner.removeSuffix("$" + "Companion"), primary.name, primary.descriptor) + } else { + out += MethodCoordinate(primary.owner + "$" + "Companion", primary.name, primary.descriptor) + } + } + return out.toList() + } + + /** + * 构建 handler —— 要求用户方法签名为 `fun(theatre: Theatre): R`。 + */ + private fun buildHandler(owner: ReflexClass, instance: Any, m: ClassMethod): (Theatre) -> Any? { + val cls = Class.forName(owner.name) + val jmethod = cls.declaredMethods.firstOrNull { it.name == m.name && it.parameterCount == 1 } ?: run { + Forensics.warn("@Surgeon 无法定位方法 ${owner.name}#${m.name},期望签名 fun(Theatre)") + return { _ -> null } + } + jmethod.isAccessible = true + return { theatre -> + try { + jmethod.invoke(instance, theatre) + } catch (e: java.lang.reflect.InvocationTargetException) { + throw e.cause ?: e + } + } + } + + private fun extractMethodDescriptor(scope: String): String { + val trimmed = scope.trim() + if ('&' !in trimmed && '|' !in trimmed && !trimmed.startsWith("!") && !trimmed.startsWith("(")) { + return trimmed.removePrefix("method:").removePrefix("class:").trim() + } + val idx = trimmed.indexOf("method:") + if (idx < 0) return trimmed + var i = idx + "method:".length + var depth = 0 + var stop = false + while (i < trimmed.length) { + when (trimmed[i]) { + '(' -> depth++ + ')' -> if (depth > 0) depth-- + '&', '|' -> if (depth == 0) stop = true + else -> Unit + } + if (stop) break + i++ + } + return trimmed.substring(idx + "method:".length, i).trim() + } + + private fun isWholeMethodBypass(site: Site, rawPattern: Any?): Boolean { + return site.anchor == Anchor.HEAD && + site.target.isBlank() && + site.shift == taboolib.module.incision.api.Shift.BEFORE && + site.ordinal == -1 && + site.offset == 0 && + readInsnSteps(rawPattern).isEmpty() + } + + private data class AdviceDeclaration( + val kind: AdviceKind, + val rawDescriptor: String, + val explicitResumeRequired: Boolean = false, + val siteSpec: SiteSpec? = null, + val insnSteps: List = emptyList(), + val patternDeclared: Boolean = false, + val where: String = "", + val onThrow: Boolean = true, + ) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ByteBuddyAttacher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ByteBuddyAttacher.kt new file mode 100644 index 000000000..afad638cd --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ByteBuddyAttacher.kt @@ -0,0 +1,22 @@ +package taboolib.module.incision.loader.attach + +import java.lang.instrument.Instrumentation + +/** + * byte-buddy-agent 路径 — 若 `net.bytebuddy:byte-buddy-agent` 在 classpath, + * 反射调用 `ByteBuddyAgent.install()` 拿到 Instrumentation。 + */ +object ByteBuddyAttacher { + + fun tryInstall(): Instrumentation? { + return try { + val cls = Class.forName("net.bytebuddy.agent.ByteBuddyAgent") + val method = cls.getMethod("install") + method.invoke(null) as? Instrumentation + } catch (_: ClassNotFoundException) { + null + } catch (t: Throwable) { + throw t + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ManualSelfAttach.kt b/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ManualSelfAttach.kt new file mode 100644 index 000000000..20f251f3b --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/loader/attach/ManualSelfAttach.kt @@ -0,0 +1,134 @@ +package taboolib.module.incision.loader.attach + +import taboolib.module.incision.diagnostic.Trauma +import java.io.File +import java.io.FileOutputStream +import java.lang.instrument.Instrumentation +import java.lang.management.ManagementFactory +import java.net.URLClassLoader +import java.util.jar.Attributes +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream +import java.util.jar.Manifest + +/** + * 手写 self-attach 兜底路径 — 不依赖 byte-buddy-agent。 + * + * JDK 8: + * - 通过 `JAVA_HOME/lib/tools.jar` 反射加载 `com.sun.tools.attach.VirtualMachine` + * - 动态生成一个最小 agent jar(含 Agent-Class / Can-Retransform-Classes manifest) + * - `VirtualMachine.attach(pid).loadAgent(jar)` → agent `agentmain` 把 Instrumentation 回填到 [captured] + * + * JDK 9+: + * - 直接使用内建 `com.sun.tools.attach`(jdk.attach 模块) + * - 若进程未启用 `jdk.attach.allowAttachSelf=true`,尝试动态 set 系统属性后再 attach + */ +object ManualSelfAttach { + + /** IncisionSelfAgent.agentmain 回填点 */ + @Volatile + @JvmStatic + var captured: Instrumentation? = null + + fun attach(): Instrumentation { + captured?.let { return it } + + // 启用 self-attach(JDK 9+) + System.setProperty("jdk.attach.allowAttachSelf", "true") + + val vmClass = loadVirtualMachineClass() + val pid = currentPid().takeIf { it > 0 } + ?: throw Trauma.Attach.InstrumentationUnavailable("无法获取当前进程 PID") + val agentJar = materializeAgentJar() + + val vmInstance = try { + vmClass.getMethod("attach", String::class.java).invoke(null, pid.toString()) + } catch (t: Throwable) { + if (isAttachSelfDisabled(t)) throw Trauma.Attach.AttachSelfDisabled() + if (isReflectionBlocked(t)) throw Trauma.Attach.ReflectionBlocked("jdk.attach/sun.tools.attach", t) + throw Trauma.Attach.AgentLoadFailed(agentJar.absolutePath, t) + } + try { + vmClass.getMethod("loadAgent", String::class.java, String::class.java) + .invoke(vmInstance, agentJar.absolutePath, ManualSelfAttach::class.java.name) + } catch (t: Throwable) { + throw Trauma.Attach.AgentLoadFailed(agentJar.absolutePath, t) + } finally { + try { vmClass.getMethod("detach").invoke(vmInstance) } catch (_: Throwable) {} + } + return captured + ?: throw Trauma.Attach.InstrumentationUnavailable("agent 已加载但未回填 Instrumentation(可能是 IncisionSelfAgent 未被打进 jar)") + } + + private fun loadVirtualMachineClass(): Class<*> { + return try { + Class.forName("com.sun.tools.attach.VirtualMachine") + } catch (_: ClassNotFoundException) { + // JDK 8:从 tools.jar 加载 + val javaHome = System.getProperty("java.home") + val candidates = listOf( + File(javaHome, "lib/tools.jar"), + File(File(javaHome).parentFile, "lib/tools.jar"), + File(javaHome, "../lib/tools.jar"), + ) + val toolsJar = candidates.firstOrNull { it.exists() } + ?: throw Trauma.Attach.NoToolsJar(javaHome) + val cl = URLClassLoader(arrayOf(toolsJar.toURI().toURL()), ClassLoader.getSystemClassLoader()) + Class.forName("com.sun.tools.attach.VirtualMachine", true, cl) + } + } + + private fun currentPid(): Long { + val name = ManagementFactory.getRuntimeMXBean().name + val at = name.indexOf('@') + return if (at > 0) name.substring(0, at).toLongOrNull() ?: -1L else -1L + } + + private fun isAttachSelfDisabled(t: Throwable): Boolean { + var cur: Throwable? = t + while (cur != null) { + val m = cur.message + if (m != null && (m.contains("allowAttachSelf", true) || m.contains("attach to self", true))) return true + cur = cur.cause + } + return false + } + + private fun isReflectionBlocked(t: Throwable): Boolean { + var cur: Throwable? = t + while (cur != null) { + val m = cur.message + if (m != null && (m.contains("module ", true) && m.contains("does not", true))) return true + cur = cur.cause + } + return false + } + + /** 动态合成最小 agent jar — manifest + IncisionSelfAgent.class */ + private fun materializeAgentJar(): File { + val tmp = File.createTempFile("incision-self-agent-", ".jar") + tmp.deleteOnExit() + + // 用运行时实际类名(重定位后可能有前缀) + val agentClass = IncisionSelfAgent::class.java + val agentClassName = agentClass.name + val agentClassPath = agentClassName.replace('.', '/') + ".class" + + val manifest = Manifest().apply { + mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0" + mainAttributes.putValue("Agent-Class", agentClassName) + mainAttributes.putValue("Premain-Class", agentClassName) + mainAttributes.putValue("Can-Retransform-Classes", "true") + mainAttributes.putValue("Can-Redefine-Classes", "true") + } + JarOutputStream(FileOutputStream(tmp), manifest).use { jar -> + val bytes = agentClass.classLoader.getResourceAsStream(agentClassPath)?.readBytes() + ?: ClassLoader.getSystemResourceAsStream(agentClassPath)?.readBytes() + ?: throw Trauma.Attach.InstrumentationUnavailable("定位不到 $agentClassPath") + jar.putNextEntry(JarEntry(agentClassPath)) + jar.write(bytes) + jar.closeEntry() + } + return tmp + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt new file mode 100644 index 000000000..0aa247174 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt @@ -0,0 +1,16 @@ +package taboolib.module.incision.pred + +/** + * 谓词编译上下文。由 advice 注册方提供,传给 [PredCompiler.compile]。 + * + * @property adviceId advice id,仅用于错误诊断(出现在 [taboolib.module.incision.diagnostic.Trauma.Predicate.RuntimeFailure] 中)。 + * @property classLoader 生成的谓词类的装载目标 ClassLoader。通常是声明 advice 的插件主 CL; + * 后续 script / external 场景可指向脚本沙箱 CL。 + * @property extraVars 除默认 `args/this/result/env/site/caller` 外允许出现的顶层变量名。 + * 未列入白名单的变量会在编译期抛 [taboolib.module.incision.diagnostic.Trauma.Predicate.UndefinedVariable]。 + */ +data class AdviceCtx( + val adviceId: String, + val classLoader: ClassLoader, + val extraVars: Set = emptySet(), +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContext.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContext.kt new file mode 100644 index 000000000..ab0b8ce30 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContext.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.pred + +/** + * 谓词求值上下文。 + * + * 由 [taboolib.module.incision.runtime.TheatreDispatcher] 在每次调用 advice 前构造, + * 用于把 `args / this / result / env / site / caller` 等 DSL 顶层变量按需暴露给已编译的 [Predicate].test。 + * + * 设计原则(懒求值): + * - 大多数 advice 的谓词不会真正使用 `result()`(只在 TRAIL 阶段有意义)或 `caller()`(昂贵的栈回溯), + * 因此这些方法应在第一次调用时才计算 / 反射,结果缓存到下一次 reset。 + * - 由 dispatcher 持有一个池化的 EvalContext 实例,每条 advice 调用前 reset。 + */ +interface EvalContext { + + /** 第 i 个原方法实参(含可空)。i 越界时抛 IndexOutOfBoundsException —— 由编译器在 `args.size()` 等场景规避。 */ + fun argAt(i: Int): Any? + + /** 原方法实参个数。 */ + fun argCount(): Int + + /** 原方法的 this(静态方法 → null)。 */ + fun thisRef(): Any? + + /** 原方法的返回值(仅 TRAIL 阶段有效;其他阶段 → null)。 */ + fun result(): Any? + + /** 用户在 advice 注册时附加的环境 map(只读视图)。 */ + fun env(): Map + + /** 当前切术站点信息(owner / name / desc,可能为 null —— 解析失败或非站点谓词)。 */ + fun site(): Any? + + /** 调用方信息(栈回溯,按需懒计算)。 */ + fun caller(): Any? +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContextImpl.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContextImpl.kt new file mode 100644 index 000000000..01726c6cb --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/EvalContextImpl.kt @@ -0,0 +1,52 @@ +package taboolib.module.incision.pred + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Theatre + +/** + * 基于 [Theatre] 的 [EvalContext] 实现,每次 advice 调用前由 dispatcher 构造并复用。 + * + * 懒求值字段(`callerCache`)在第一次访问时才计算;其他字段直接代理 Theatre。 + * + * 不可跨线程复用:dispatcher 为每次调用 new 一个实例。如需池化由调用方在 reset 时 + * 清掉 [callerCache]。 + */ +internal class EvalContextImpl( + private val theatre: Theatre, + private val coord: MethodCoordinate, + private val resultProvider: () -> Any?, + private val envMap: Map = emptyMap(), +) : EvalContext { + + private object UNSET + private var callerCache: Any? = UNSET + + override fun argAt(i: Int): Any? = theatre.args[i] + + override fun argCount(): Int = theatre.args.size + + override fun thisRef(): Any? = theatre.self + + override fun result(): Any? = resultProvider() + + override fun env(): Map = envMap + + override fun site(): Any? = coord + + override fun caller(): Any? { + if (callerCache !== UNSET) return callerCache + // 跳过 Throwable / EvalContextImpl / dispatcher / weaver 自身的栈帧 + val st = Throwable().stackTrace + var found: StackTraceElement? = null + for (e in st) { + val cn = e.className + if (cn.startsWith("taboolib.module.incision.")) continue + if (cn.startsWith("io.izzel.incision.")) continue + if (cn.startsWith("java.lang.")) continue + found = e + break + } + callerCache = found + return found + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredAst.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredAst.kt new file mode 100644 index 000000000..43faae47d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredAst.kt @@ -0,0 +1,59 @@ +package taboolib.module.incision.pred + +/** + * 谓词 DSL 抽象语法树。 + * + * 类型算子语义(语义层一元算子 `!` 与 `is/ic/ip/it` 在 [TypeCheck.negate] 中组合): + * - `is T` → `T.isInstance(x)` + * - `ic T` → INSTANCEOF + 不等 class(即 isInstance 且 x.class != T) + * - `ip T` → `T.isAssignableFrom(x.class)` + * - `it T` → `x.class == T` + * - `as T` → 转换失败时谓词整体取 false(不抛) + */ +internal sealed class PredAst { + + /** `a || b || c` */ + data class Or(val items: List) : PredAst() + + /** `a && b && c` */ + data class And(val items: List) : PredAst() + + /** `!expr` 语义层一元 */ + data class Not(val target: PredAst) : PredAst() + + /** 二元比较,op∈ {==,!=,<,>,<=,>=,matches,in} */ + data class Cmp(val op: String, val left: PredAst, val right: PredAst) : PredAst() + + /** + * 类型检查算子。 + * - kind ∈ {IS, IC, IP, IT} + * - [negate] 表示是否带前缀 `!`(由 Parser 在 type_op 路径或 unary 路径合成) + */ + data class TypeCheck(val target: PredAst, val kind: Kind, val typeName: String, val negate: Boolean) : PredAst() { + enum class Kind { IS, IC, IP, IT } + } + + /** `expr as T`:失败语义 → 整体谓词为 false(运行期) */ + data class As(val target: PredAst, val typeName: String) : PredAst() + + /** `.ident` 属性访问 */ + data class PropertyAccess(val receiver: PredAst, val name: String) : PredAst() + + /** `.ident(args)` 方法调用 */ + data class MethodCall(val receiver: PredAst, val name: String, val args: List) : PredAst() + + /** `[expr]` 下标访问 */ + data class Index(val receiver: PredAst, val index: PredAst) : PredAst() + + /** `?.` 安全访问标记,作用于其子节点(PropertyAccess/MethodCall),缺省语义:null 短路返回 false。 */ + data class SafeAccess(val target: PredAst) : PredAst() + + /** 顶层标识符引用,例如 `args` / `result` / `this` / `env` */ + data class Var(val name: String) : PredAst() + + /** 字面量:Int / Long / Double / String / Boolean / null */ + data class Literal(val value: Any?) : PredAst() + + /** `( expr )` —— 保留括号节点便于诊断输出,编译期等价于内部表达式 */ + data class Paren(val inner: PredAst) : PredAst() +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt new file mode 100644 index 000000000..0eaef0817 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt @@ -0,0 +1,496 @@ +package taboolib.module.incision.pred + +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.JvmtiBackend +import java.lang.reflect.Method +import java.util.concurrent.atomic.AtomicInteger + +/** + * 谓词字节码编译器:把 [PredAst] 编译成实现 [Predicate] 接口的匿名类,并装载到目标 ClassLoader。 + * + * 生成策略: + * - 一个方法 `test(Lcom/.../EvalContext;)Z`,方法体里所有中间表达式以 `Object` 形式驻留栈顶; + * 最终 `INVOKESTATIC PredOps.truthy(Object)Z` 转为 boolean 返回。 + * - 类型算子、比较、成员访问、下标全部下发到 [PredOps],保持字节码体积小、栈类型简单。 + * - 类型字面量(`is/ic/ip/it/as` 后的 `T`)走 `PredOps.resolveType(name, cl)`,cl 取自当前线程 contextClassLoader; + * 若无法解析视为永假分支(asCast 返回 null,类型检查返回 false)。 + * + * 上下文([AdviceCtx])来自调用方,用于: + * - 选择装载目标 ClassLoader(写到 advice 注册方的插件 CL,后续 script 场景可指向沙箱 CL) + * - 提供 advice id 用于错误诊断 + * - 通过 [AdviceCtx.extraVars] 扩展顶层变量白名单(默认 `args/this/result/env/site/caller`) + * + * 调用方式: + * ```kotlin + * val pred = PredCompiler.compile("args[0] is java.lang.String && result != null", + * AdviceCtx("my.advice.id", pluginClassLoader)) + * // 注册期完成;运行期 dispatcher 直接 pred.test(ctx) + * ``` + */ +object PredCompiler { + + private val seq = AtomicInteger(0) + + private val DEFAULT_VARS = setOf("args", "this", "result", "env", "site", "caller") + + private val OPS_INTERNAL = PredOps::class.java.name.replace('.', '/') + private val PRED_INTERNAL = Predicate::class.java.name.replace('.', '/') + private val EVAL_INTERNAL = EvalContext::class.java.name.replace('.', '/') + private val GEN_PACKAGE = PRED_INTERNAL.substringBeforeLast('/') + "/gen" + private const val OBJ = "Ljava/lang/Object;" + + /** + * 入口:源码 → AST → 字节码 → 装载 → 实例。注册期一次性调用,运行期复用返回的 [Predicate]。 + * + * @param source 谓词源码(DSL 字符串)。 + * @param ctx [AdviceCtx],提供装载 ClassLoader、advice id 与变量白名单。 + * @return 已装载并实例化的 [Predicate],永不返回 null(失败一律抛异常)。 + * + * @throws taboolib.module.incision.diagnostic.Trauma.Predicate.SyntaxError + * 词法 / 语法错误,例如未闭合的字符串、意外 token、`expr` 末尾多余内容、`!as` 等不合法组合。 + * @throws taboolib.module.incision.diagnostic.Trauma.Predicate.MethodIndexed + * 在方法调用结果上做下标访问(如 `args.size[0]`)。 + * @throws taboolib.module.incision.diagnostic.Trauma.Predicate.UndefinedVariable + * 引用了不在默认 6 项 + [AdviceCtx.extraVars] 之内的顶层变量。 + * @throws taboolib.module.incision.diagnostic.Trauma.Predicate.RuntimeFailure + * 字节码生成成功但 ClassLoader.defineClass 失败(被包装抛出)。 + * + * 注:[Trauma.Predicate.UnknownMember] / [Trauma.Predicate.TypeMismatch] 不在编译期抛出, + * 它们由运行期 [PredOps] 在反射兜底失败时由上层 dispatcher 包装。 + */ + fun compile(source: String, ctx: AdviceCtx): Predicate { + val ast = PredParser(source).parse() + validate(source, ast, ctx) + val (className, bytes) = generate(ast, ctx, source) + val cls = LoaderHelper.define(ctx.classLoader, className, bytes) + return cls.getDeclaredConstructor().newInstance() as Predicate + } + + // ---------- 校验:未定义变量、方法结果做下标已在 Parser 拦截 ---------- + + private fun validate(source: String, ast: PredAst, ctx: AdviceCtx) { + val allowed = DEFAULT_VARS + ctx.extraVars + walk(ast) { node -> + if (node is PredAst.Var && node.name !in allowed) { + throw Trauma.Predicate.UndefinedVariable(source, node.name) + } + } + } + + private fun walk(ast: PredAst, visit: (PredAst) -> Unit) { + visit(ast) + when (ast) { + is PredAst.Or -> ast.items.forEach { walk(it, visit) } + is PredAst.And -> ast.items.forEach { walk(it, visit) } + is PredAst.Not -> walk(ast.target, visit) + is PredAst.Cmp -> { + walk(ast.left, visit); walk(ast.right, visit) + } + + is PredAst.TypeCheck -> walk(ast.target, visit) + is PredAst.As -> walk(ast.target, visit) + is PredAst.PropertyAccess -> walk(ast.receiver, visit) + is PredAst.MethodCall -> { + walk(ast.receiver, visit); ast.args.forEach { walk(it, visit) } + } + + is PredAst.Index -> { + walk(ast.receiver, visit); walk(ast.index, visit) + } + + is PredAst.SafeAccess -> walk(ast.target, visit) + is PredAst.Paren -> walk(ast.inner, visit) + is PredAst.Var, is PredAst.Literal -> Unit + } + } + + // ---------- 生成 ---------- + + private fun generate(ast: PredAst, ctx: AdviceCtx, source: String): Pair { + val id = seq.incrementAndGet() + val internal = "$GEN_PACKAGE/Pred\$$id" + val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + cw.visit( + Opcodes.V1_8, + Opcodes.ACC_PUBLIC or Opcodes.ACC_FINAL or Opcodes.ACC_SYNTHETIC, + internal, null, "java/lang/Object", + arrayOf(PRED_INTERNAL), + ) + // + val init = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null) + init.visitCode() + init.visitVarInsn(Opcodes.ALOAD, 0) + init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false) + init.visitInsn(Opcodes.RETURN) + init.visitMaxs(0, 0) + init.visitEnd() + // test(Lcom/.../EvalContext;)Z + val mv = cw.visitMethod( + Opcodes.ACC_PUBLIC, + "test", + "(L$EVAL_INTERNAL;)Z", + null, null, + ) + mv.visitCode() + Emitter(mv, ctx).emit(ast) + mv.visitMethodInsn(Opcodes.INVOKESTATIC, OPS_INTERNAL, "truthy", "(Ljava/lang/Object;)Z", false) + mv.visitInsn(Opcodes.IRETURN) + mv.visitMaxs(0, 0) + mv.visitEnd() + cw.visitEnd() + return internal.replace('/', '.') to cw.toByteArray() + } + + // ---------- 表达式发射器 ---------- + + private class Emitter(val mv: MethodVisitor, val ctx: AdviceCtx) { + + /** 任一 emit 调用结束后,栈顶为一个 Object(含 Boolean 装箱)。 */ + fun emit(ast: PredAst) { + when (ast) { + is PredAst.Or -> emitOr(ast) + is PredAst.And -> emitAnd(ast) + is PredAst.Not -> emitNot(ast) + is PredAst.Cmp -> emitCmp(ast) + is PredAst.TypeCheck -> emitTypeCheck(ast) + is PredAst.As -> emitAs(ast) + is PredAst.PropertyAccess -> emitProperty(ast.receiver, ast.name, safe = false) + is PredAst.MethodCall -> emitMethodCall(ast.receiver, ast.name, ast.args, safe = false) + is PredAst.Index -> { + emit(ast.receiver); emit(ast.index); invokeOps("index", "(${OBJ}${OBJ})${OBJ}") + } + + is PredAst.SafeAccess -> emitSafe(ast.target) + is PredAst.Paren -> emit(ast.inner) + is PredAst.Var -> emitVar(ast.name) + is PredAst.Literal -> emitLiteral(ast.value) + } + } + + private fun emitOr(node: PredAst.Or) { + // 短路:第一个为 true 即 Boolean.TRUE + val end = Label() + val pushTrue = Label() + for (i in node.items.indices) { + emit(node.items[i]) + if (i == node.items.size - 1) break + // dup; truthy? if truthy → goto pushTrue(保留栈顶值再跳) + mv.visitInsn(Opcodes.DUP) + invokeOps("truthy", "(${OBJ})Z") + mv.visitJumpInsn(Opcodes.IFNE, pushTrue) + mv.visitInsn(Opcodes.POP) // 丢弃假值,继续下一项 + } + mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(pushTrue) + // 栈顶已是上一项的(真)值,原样保留即可 + mv.visitLabel(end) + } + + private fun emitAnd(node: PredAst.And) { + val end = Label() + val pushFalse = Label() + for (i in node.items.indices) { + emit(node.items[i]) + if (i == node.items.size - 1) break + mv.visitInsn(Opcodes.DUP) + invokeOps("truthy", "(${OBJ})Z") + mv.visitJumpInsn(Opcodes.IFEQ, pushFalse) + mv.visitInsn(Opcodes.POP) + } + mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(pushFalse) + // 栈顶是假值,保留 + mv.visitLabel(end) + } + + private fun emitNot(node: PredAst.Not) { + emit(node.target) + invokeOps("truthy", "(${OBJ})Z") + // boolean → !boolean → Boolean + val falseL = Label(); + val end = Label() + mv.visitJumpInsn(Opcodes.IFNE, falseL) + getStaticBoolean(true) + mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(falseL) + getStaticBoolean(false) + mv.visitLabel(end) + } + + private fun emitCmp(node: PredAst.Cmp) { + emit(node.left) + emit(node.right) + val (name, desc) = when (node.op) { + "==" -> "eq" to "(${OBJ}${OBJ})Z" + "!=" -> "neq" to "(${OBJ}${OBJ})Z" + "<" -> "lt" to "(${OBJ}${OBJ})Z" + ">" -> "gt" to "(${OBJ}${OBJ})Z" + "<=" -> "le" to "(${OBJ}${OBJ})Z" + ">=" -> "ge" to "(${OBJ}${OBJ})Z" + "matches" -> "matches" to "(${OBJ}${OBJ})Z" + "in" -> "contains" to "(${OBJ}${OBJ})Z" + else -> error("unknown cmp op ${node.op}") + } + invokeOps(name, desc) + boxBoolean() + } + + private fun emitTypeCheck(node: PredAst.TypeCheck) { + emit(node.target) + emitResolveType(node.typeName) + val name = when (node.kind) { + PredAst.TypeCheck.Kind.IS -> "isInstanceOf" + PredAst.TypeCheck.Kind.IC -> "isInstanceChild" + PredAst.TypeCheck.Kind.IP -> "isAssignable" + PredAst.TypeCheck.Kind.IT -> "isExactType" + } + invokeOps(name, "(${OBJ}Ljava/lang/Class;)Z") + if (node.negate) { + val falseL = Label(); + val end = Label() + mv.visitJumpInsn(Opcodes.IFNE, falseL) + getStaticBoolean(true); mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(falseL); getStaticBoolean(false) + mv.visitLabel(end) + } else { + boxBoolean() + } + } + + private fun emitAs(node: PredAst.As) { + emit(node.target) + emitResolveType(node.typeName) + invokeOps("asCast", "(${OBJ}Ljava/lang/Class;)${OBJ}") + } + + private fun emitProperty(receiver: PredAst, name: String, safe: Boolean) { + emit(receiver) + if (safe) emitNullShortCircuit { /* falls through with object */ } + mv.visitLdcInsn(name) + invokeOps("getProperty", "(${OBJ}Ljava/lang/String;)${OBJ}") + } + + private fun emitMethodCall(receiver: PredAst, name: String, args: List, safe: Boolean) { + emit(receiver) + if (safe) emitNullShortCircuit { /* */ } + mv.visitLdcInsn(name) + // new Object[args.size] + pushInt(args.size) + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") + for ((i, a) in args.withIndex()) { + mv.visitInsn(Opcodes.DUP) + pushInt(i) + emit(a) + mv.visitInsn(Opcodes.AASTORE) + } + invokeOps("callMethod", "(${OBJ}Ljava/lang/String;[Ljava/lang/Object;)${OBJ}") + } + + /** + * `?.`:把内部 PropertyAccess/MethodCall 在 receiver=null 时短路为 null。 + * 实现:包装一层判空 —— 如果 receiver 就是 null 直接走 null 分支。 + */ + private fun emitSafe(inner: PredAst) { + when (inner) { + is PredAst.PropertyAccess -> { + emit(inner.receiver) + val nullL = Label(); + val end = Label() + mv.visitInsn(Opcodes.DUP) + mv.visitJumpInsn(Opcodes.IFNULL, nullL) + mv.visitLdcInsn(inner.name) + invokeOps("getProperty", "(${OBJ}Ljava/lang/String;)${OBJ}") + mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(nullL) + // 栈顶是 null,保留 + mv.visitLabel(end) + } + + is PredAst.MethodCall -> { + emit(inner.receiver) + val nullL = Label(); + val end = Label() + mv.visitInsn(Opcodes.DUP) + mv.visitJumpInsn(Opcodes.IFNULL, nullL) + mv.visitLdcInsn(inner.name) + pushInt(inner.args.size) + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") + for ((i, a) in inner.args.withIndex()) { + mv.visitInsn(Opcodes.DUP) + pushInt(i) + emit(a) + mv.visitInsn(Opcodes.AASTORE) + } + invokeOps("callMethod", "(${OBJ}Ljava/lang/String;[Ljava/lang/Object;)${OBJ}") + mv.visitJumpInsn(Opcodes.GOTO, end) + mv.visitLabel(nullL) + mv.visitLabel(end) + } + + else -> emit(inner) // 非访问节点的安全前缀语义等价于裸求值 + } + } + + /** 占位:保留以便未来补充更复杂的短路路径(当前 emitSafe 中已用内联实现)。 */ + private inline fun emitNullShortCircuit(crossinline body: () -> Unit) { + body() + } + + private fun emitVar(name: String) { + // ALOAD 1 = EvalContext + mv.visitVarInsn(Opcodes.ALOAD, 1) + when (name) { + "args" -> { + // 顶层 args 暴露为 Object[](通过 EvalContext.argCount + argAt 循环填充) + emitCollectArgs() + } + + "this" -> mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "thisRef", "()${OBJ}", true) + "result" -> mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "result", "()${OBJ}", true) + "env" -> mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "env", "()Ljava/util/Map;", true) + "site" -> mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "site", "()${OBJ}", true) + "caller" -> mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "caller", "()${OBJ}", true) + else -> { + // 自定义变量:尝试 env().get(name) + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "env", "()Ljava/util/Map;", true) + mv.visitLdcInsn(name) + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Map", "get", "(${OBJ})${OBJ}", true) + } + } + } + + private fun emitCollectArgs() { + // 入参:栈顶为 EvalContext 副本(emitVar 已 ALOAD 1 一次)。 + // 直接丢弃,改用局部变量法构造 Object[]。slots: 2=n, 3=arr, 4=i + mv.visitInsn(Opcodes.POP) + mv.visitVarInsn(Opcodes.ALOAD, 1) + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "argCount", "()I", true) + mv.visitVarInsn(Opcodes.ISTORE, 2) + mv.visitVarInsn(Opcodes.ILOAD, 2) + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") + mv.visitVarInsn(Opcodes.ASTORE, 3) + mv.visitInsn(Opcodes.ICONST_0) + mv.visitVarInsn(Opcodes.ISTORE, 4) + val loop = Label(); + val end = Label() + mv.visitLabel(loop) + mv.visitVarInsn(Opcodes.ILOAD, 4) + mv.visitVarInsn(Opcodes.ILOAD, 2) + mv.visitJumpInsn(Opcodes.IF_ICMPGE, end) + mv.visitVarInsn(Opcodes.ALOAD, 3) + mv.visitVarInsn(Opcodes.ILOAD, 4) + mv.visitVarInsn(Opcodes.ALOAD, 1) + mv.visitVarInsn(Opcodes.ILOAD, 4) + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, EVAL_INTERNAL, "argAt", "(I)${OBJ}", true) + mv.visitInsn(Opcodes.AASTORE) + mv.visitIincInsn(4, 1) + mv.visitJumpInsn(Opcodes.GOTO, loop) + mv.visitLabel(end) + mv.visitVarInsn(Opcodes.ALOAD, 3) + } + + private fun emitLiteral(v: Any?) { + when (v) { + null -> mv.visitInsn(Opcodes.ACONST_NULL) + is Boolean -> getStaticBoolean(v) + is Int -> { + pushInt(v); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false) + } + + is Long -> { + mv.visitLdcInsn(v); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false) + } + + is Double -> { + mv.visitLdcInsn(v); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false) + } + + is String -> mv.visitLdcInsn(v) + else -> mv.visitLdcInsn(v.toString()) + } + } + + private fun emitResolveType(typeName: String) { + mv.visitLdcInsn(typeName) + // 把当前线程 contextClassLoader 传入 + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false) + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Thread", "getContextClassLoader", "()Ljava/lang/ClassLoader;", false) + invokeOps("resolveType", "(Ljava/lang/String;Ljava/lang/ClassLoader;)Ljava/lang/Class;") + } + + // helpers + private fun invokeOps(name: String, desc: String) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, OPS_INTERNAL, name, desc, false) + } + + private fun boxBoolean() { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false) + } + + private fun getStaticBoolean(b: Boolean) { + mv.visitFieldInsn( + Opcodes.GETSTATIC, "java/lang/Boolean", + if (b) "TRUE" else "FALSE", "Ljava/lang/Boolean;", + ) + } + + private fun pushInt(i: Int) { + when { + i in -1..5 -> mv.visitInsn(Opcodes.ICONST_0 + i) + i in Byte.MIN_VALUE..Byte.MAX_VALUE -> mv.visitIntInsn(Opcodes.BIPUSH, i) + i in Short.MIN_VALUE..Short.MAX_VALUE -> mv.visitIntInsn(Opcodes.SIPUSH, i) + else -> mv.visitLdcInsn(i) + } + } + } + + // ---------- 类装载器:反射优先;JDK 9+ 模块边界失败则退到 JNI DefineClass ---------- + + private object LoaderHelper { + private val defineMethod: Method? by lazy { + try { + val m = ClassLoader::class.java.getDeclaredMethod( + "defineClass", + String::class.java, + ByteArray::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ) + m.isAccessible = true + m + } catch (_: Throwable) { + null + } + } + + fun define(cl: ClassLoader, name: String, bytes: ByteArray): Class<*> { + val binaryName = name.replace('/', '.') + try { + return Class.forName(binaryName, false, cl) + } catch (_: Throwable) { + } + val reflectError: Throwable? = defineMethod?.let { m -> + try { + return m.invoke(cl, binaryName, bytes, 0, bytes.size) as Class<*> + } catch (t: Throwable) { + t + } + } + // JDK 9+ 或反射被禁:退到 native JNI DefineClass(不受模块系统限制) + try { + val cls = JvmtiBackend.defineClassInClassLoader(cl, name, bytes) + if (cls != null) return cls + } catch (_: Throwable) { + // native 不可用时继续抛原反射异常 + } + throw Trauma.Predicate.RuntimeFailure( + "", null, + reflectError ?: IllegalStateException("defineClass unavailable (reflection + JVMTI both failed)") + ) + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredLexer.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredLexer.kt new file mode 100644 index 000000000..aa00e6f7d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredLexer.kt @@ -0,0 +1,148 @@ +package taboolib.module.incision.pred + +import taboolib.module.incision.diagnostic.Trauma + +/** + * 谓词 DSL 词法分析器。 + * + * 关键规则: + * - `!` 单独 token(PredParser 在语义层组合 `!is` / `!ic` / `!ip` / `!it`,不在词法层预合成)。 + * - 类型算子 `as / is / ic / ip / it / matches / in` 走 Ident 路径,由 Parser 按位置识别。 + * - 数字字面量支持整数与小数;字符串用单/双引号;布尔 / null 走关键字 ident。 + * - 标识符:[A-Za-z_$][A-Za-z0-9_$]*,类型名(type_name)允许 `.`,由 Parser 在 type_op 后串联读取。 + */ +internal class PredLexer(private val source: String) { + + enum class Kind { + // 字面量 + Number, String, True, False, Null, + // 标识符 + Ident, + // 运算符 / 标点 + OrOr, AndAnd, Bang, + Eq, Neq, Lt, Gt, Le, Ge, + Dot, SafeDot, Comma, + LParen, RParen, LBracket, RBracket, + Eof + } + + data class Token(val kind: Kind, val text: String, val pos: Int) + + private var pos = 0 + private val len = source.length + + fun tokenize(): List { + val out = ArrayList(16) + while (true) { + val tk = nextToken() ?: continue + out += tk + if (tk.kind == Kind.Eof) return out + } + } + + private fun nextToken(): Token? { + skipSpaces() + if (pos >= len) return Token(Kind.Eof, "", pos) + val start = pos + val c = source[pos] + return when { + c.isDigit() -> readNumber(start) + c == '"' || c == '\'' -> readString(start, c) + isIdentStart(c) -> readIdent(start) + else -> readSymbol(start, c) + } + } + + private fun skipSpaces() { + while (pos < len) { + val c = source[pos] + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++ else break + } + } + + private fun readNumber(start: Int): Token { + while (pos < len && source[pos].isDigit()) pos++ + if (pos < len && source[pos] == '.' && pos + 1 < len && source[pos + 1].isDigit()) { + pos++ + while (pos < len && source[pos].isDigit()) pos++ + } + return Token(Kind.Number, source.substring(start, pos), start) + } + + private fun readString(start: Int, quote: Char): Token { + pos++ // skip opening quote + val sb = StringBuilder() + while (pos < len) { + val c = source[pos] + if (c == '\\' && pos + 1 < len) { + val nx = source[pos + 1] + sb.append( + when (nx) { + 'n' -> '\n'; 't' -> '\t'; 'r' -> '\r' + '\\' -> '\\'; '\'' -> '\''; '"' -> '"' + else -> nx + } + ) + pos += 2 + continue + } + if (c == quote) { + pos++ + return Token(Kind.String, sb.toString(), start) + } + sb.append(c); pos++ + } + throw Trauma.Predicate.SyntaxError(source, start, "字符串字面量未闭合") + } + + private fun readIdent(start: Int): Token { + while (pos < len && isIdentPart(source[pos])) pos++ + val text = source.substring(start, pos) + val kind = when (text) { + "true" -> Kind.True + "false" -> Kind.False + "null" -> Kind.Null + else -> Kind.Ident + } + return Token(kind, text, start) + } + + private fun readSymbol(start: Int, c: Char): Token { + return when (c) { + '|' -> { + if (peek(1) == '|') { pos += 2; Token(Kind.OrOr, "||", start) } + else throw Trauma.Predicate.SyntaxError(source, start, "未识别字符 '|',是否想写 '||'?") + } + '&' -> { + if (peek(1) == '&') { pos += 2; Token(Kind.AndAnd, "&&", start) } + else throw Trauma.Predicate.SyntaxError(source, start, "未识别字符 '&',是否想写 '&&'?") + } + '!' -> { + if (peek(1) == '=') { pos += 2; Token(Kind.Neq, "!=", start) } + else { pos++; Token(Kind.Bang, "!", start) } + } + '=' -> { + if (peek(1) == '=') { pos += 2; Token(Kind.Eq, "==", start) } + else throw Trauma.Predicate.SyntaxError(source, start, "未识别字符 '=',是否想写 '=='?") + } + '<' -> if (peek(1) == '=') { pos += 2; Token(Kind.Le, "<=", start) } else { pos++; Token(Kind.Lt, "<", start) } + '>' -> if (peek(1) == '=') { pos += 2; Token(Kind.Ge, ">=", start) } else { pos++; Token(Kind.Gt, ">", start) } + '?' -> { + if (peek(1) == '.') { pos += 2; Token(Kind.SafeDot, "?.", start) } + else throw Trauma.Predicate.SyntaxError(source, start, "未识别字符 '?',是否想写 '?.'?") + } + '.' -> { pos++; Token(Kind.Dot, ".", start) } + ',' -> { pos++; Token(Kind.Comma, ",", start) } + '(' -> { pos++; Token(Kind.LParen, "(", start) } + ')' -> { pos++; Token(Kind.RParen, ")", start) } + '[' -> { pos++; Token(Kind.LBracket, "[", start) } + ']' -> { pos++; Token(Kind.RBracket, "]", start) } + else -> throw Trauma.Predicate.SyntaxError(source, start, "未识别字符 '$c'") + } + } + + private fun peek(offset: Int): Char = if (pos + offset < len) source[pos + offset] else '\u0000' + + private fun isIdentStart(c: Char) = c.isLetter() || c == '_' || c == '$' + private fun isIdentPart(c: Char) = c.isLetterOrDigit() || c == '_' || c == '$' +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredParser.kt new file mode 100644 index 000000000..0edefbcf6 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredParser.kt @@ -0,0 +1,281 @@ +package taboolib.module.incision.pred + +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.pred.PredLexer.Kind +import taboolib.module.incision.pred.PredLexer.Token + +/** + * 谓词 DSL 递归下降语法分析器。 + * + * 文法(已锁定,见 #9): + * ``` + * expr := or + * or := and ('||' and)* + * and := type_cmp ('&&' type_cmp)* + * type_cmp := cmp (type_op type_name)? + * type_op := 'as' | 'is' | '!is' | 'ic' | '!ic' | 'ip' | '!ip' | 'it' | '!it' + * cmp := unary (('==' | '!=' | '<' | '>' | '<=' | '>=' | 'matches' | 'in') unary)? + * unary := '!' unary | postfix + * postfix := primary access* + * access := '.' ident + * | '.' ident '(' args? ')' + * | '[' expr ']' + * | '?.' (ident | ident '(' args? ')') + * primary := literal | ident | '(' expr ')' + * ``` + * + * 关键约束: + * - `args.size[0]` 等"方法结果再做下标"的形式,由 [accessLoop] 在产出 [PredAst.MethodCall] 后立即看见 `[` 时 + * 抛 `Trauma.Predicate.MethodIndexed`。 + * - `!is/!ic/!ip/!it` 词法层不预合成;解析 type_op 时若上一 token 是 `!` 则携带 negate=true。 + */ +internal class PredParser(private val source: String) { + + private val tokens: List = PredLexer(source).tokenize() + private var idx = 0 + + fun parse(): PredAst { + val ast = parseOr() + expect(Kind.Eof, "表达式末尾存在多余内容") + return ast + } + + // ---------- or / and ---------- + + private fun parseOr(): PredAst { + val first = parseAnd() + if (peek().kind != Kind.OrOr) return first + val items = ArrayList(2).apply { add(first) } + while (peek().kind == Kind.OrOr) { advance(); items += parseAnd() } + return PredAst.Or(items) + } + + private fun parseAnd(): PredAst { + val first = parseTypeCmp() + if (peek().kind != Kind.AndAnd) return first + val items = ArrayList(2).apply { add(first) } + while (peek().kind == Kind.AndAnd) { advance(); items += parseTypeCmp() } + return PredAst.And(items) + } + + // ---------- type_cmp ---------- + + private fun parseTypeCmp(): PredAst { + val left = parseCmp() + // 此处的 `!is` 等通过先看 `!` 再看 ident 实现 + val (op, negate, opPos) = peekTypeOp() ?: return left + // 消耗 token:可能是 1 个(is/ic/ip/it/as)或 2 个(! + is/ic/ip/it) + if (negate) advance() // ! + advance() // type_op ident + val typeName = readTypeName(opPos) + return when (op) { + "as" -> { + if (negate) throw Trauma.Predicate.SyntaxError(source, opPos, "'as' 不支持 '!as' 形式") + PredAst.As(left, typeName) + } + "is" -> PredAst.TypeCheck(left, PredAst.TypeCheck.Kind.IS, typeName, negate) + "ic" -> PredAst.TypeCheck(left, PredAst.TypeCheck.Kind.IC, typeName, negate) + "ip" -> PredAst.TypeCheck(left, PredAst.TypeCheck.Kind.IP, typeName, negate) + "it" -> PredAst.TypeCheck(left, PredAst.TypeCheck.Kind.IT, typeName, negate) + else -> error("unreachable") + } + } + + /** 返回 (op, negate, opPos);不消耗 token。 */ + private fun peekTypeOp(): Triple? { + val t0 = peek() + if (t0.kind == Kind.Bang) { + val t1 = peek(1) + if (t1.kind == Kind.Ident && t1.text in TYPE_OPS_NEGATABLE) { + return Triple(t1.text, true, t1.pos) + } + return null + } + if (t0.kind == Kind.Ident && t0.text in TYPE_OPS_ALL) { + return Triple(t0.text, false, t0.pos) + } + return null + } + + private fun readTypeName(opPos: Int): String { + val first = peek() + if (first.kind != Kind.Ident) { + throw Trauma.Predicate.SyntaxError(source, first.pos, "类型算子之后期望类型名") + } + val sb = StringBuilder(first.text); advance() + while (peek().kind == Kind.Dot && peek(1).kind == Kind.Ident) { + advance(); sb.append('.').append(peek().text); advance() + } + if (sb.isEmpty()) throw Trauma.Predicate.SyntaxError(source, opPos, "类型名为空") + return sb.toString() + } + + // ---------- cmp ---------- + + private fun parseCmp(): PredAst { + val left = parseUnary() + val t = peek() + val op = when (t.kind) { + Kind.Eq -> "=="; Kind.Neq -> "!=" + Kind.Lt -> "<"; Kind.Gt -> ">"; Kind.Le -> "<="; Kind.Ge -> ">=" + Kind.Ident -> when (t.text) { "matches" -> "matches"; "in" -> "in"; else -> null } + else -> null + } ?: return left + advance() + val right = parseUnary() + return PredAst.Cmp(op, left, right) + } + + // ---------- unary ---------- + + private fun parseUnary(): PredAst { + if (peek().kind == Kind.Bang) { + // 注意:若是 `!is/!ic/!ip/!it` 的形式,由 parseTypeCmp 在更外层消化; + // 此处只处理"作用于表达式"的 `!`。需要前看一位避免吞掉 `! is`。 + val n1 = peek(1) + if (!(n1.kind == Kind.Ident && n1.text in TYPE_OPS_NEGATABLE)) { + advance() + return PredAst.Not(parseUnary()) + } + } + return parsePostfix() + } + + // ---------- postfix / access ---------- + + private fun parsePostfix(): PredAst { + var node = parsePrimary() + node = accessLoop(node) + return node + } + + private fun accessLoop(start: PredAst): PredAst { + var node = start + while (true) { + val t = peek() + when (t.kind) { + Kind.Dot -> { + advance() + val nameTk = expectIdent("'.' 之后期望成员名") + node = if (peek().kind == Kind.LParen) { + val args = parseCallArgs() + val call = PredAst.MethodCall(node, nameTk.text, args) + if (peek().kind == Kind.LBracket) { + throw Trauma.Predicate.MethodIndexed(source, nameTk.text) + } + call + } else { + val prop = PredAst.PropertyAccess(node, nameTk.text) + if (peek().kind == Kind.LBracket) { + throw Trauma.Predicate.MethodIndexed(source, nameTk.text) + } + prop + } + } + Kind.SafeDot -> { + advance() + val nameTk = expectIdent("'?.' 之后期望成员名") + val inner = if (peek().kind == Kind.LParen) { + val args = parseCallArgs() + val call = PredAst.MethodCall(node, nameTk.text, args) + if (peek().kind == Kind.LBracket) { + throw Trauma.Predicate.MethodIndexed(source, nameTk.text) + } + call + } else { + val prop = PredAst.PropertyAccess(node, nameTk.text) + if (peek().kind == Kind.LBracket) { + throw Trauma.Predicate.MethodIndexed(source, nameTk.text) + } + prop + } + node = PredAst.SafeAccess(inner) + } + Kind.LBracket -> { + advance() + val idxExpr = parseOr() + expect(Kind.RBracket, "下标缺少 ']'") + node = PredAst.Index(node, idxExpr) + } + else -> return node + } + } + } + + private fun parseCallArgs(): List { + expect(Kind.LParen, "期望 '('") + if (peek().kind == Kind.RParen) { advance(); return emptyList() } + val args = ArrayList(2) + args += parseOr() + while (peek().kind == Kind.Comma) { advance(); args += parseOr() } + expect(Kind.RParen, "参数列表缺少 ')'") + return args + } + + // ---------- primary ---------- + + private fun parsePrimary(): PredAst { + val t = peek() + return when (t.kind) { + Kind.Number -> { + advance() + val v: Any = if (t.text.contains('.')) t.text.toDouble() else { + val l = t.text.toLong() + if (l in Int.MIN_VALUE..Int.MAX_VALUE) l.toInt() else l + } + PredAst.Literal(v) + } + Kind.String -> { advance(); PredAst.Literal(t.text) } + Kind.True -> { advance(); PredAst.Literal(true) } + Kind.False -> { advance(); PredAst.Literal(false) } + Kind.Null -> { advance(); PredAst.Literal(null) } + Kind.Ident -> { advance(); PredAst.Var(t.text) } + Kind.LParen -> { + advance() + val e = parseOr() + expect(Kind.RParen, "缺少 ')'") + PredAst.Paren(e) + } + else -> throw Trauma.Predicate.SyntaxError(source, t.pos, "意外的 token '${t.text}'") + } + } + + // ---------- helpers ---------- + + private fun peek(offset: Int = 0): Token = tokens[(idx + offset).coerceAtMost(tokens.size - 1)] + private fun advance(): Token = tokens[idx++] + private fun expect(kind: Kind, reason: String): Token { + val t = peek() + if (t.kind != kind) throw Trauma.Predicate.SyntaxError(source, t.pos, "$reason,实际: '${t.text}'") + return advance() + } + private fun expectIdent(reason: String): Token { + val t = peek() + if (t.kind != Kind.Ident) throw Trauma.Predicate.SyntaxError(source, t.pos, "$reason,实际: '${t.text}'") + return advance() + } + + companion object { + private val TYPE_OPS_NEGATABLE = setOf("is", "ic", "ip", "it") + private val TYPE_OPS_ALL = setOf("as", "is", "ic", "ip", "it") + + @JvmStatic + fun main(args: Array) { + val cases = listOf( + "args[0] is java.lang.String && result != null", + "this.name == 'Steve' || env.size() > 0", + "!(args[0] ic Number) && args.size() == 2", + "(result as java.util.List)?.isEmpty() == false", + "caller().name matches 'Player.*'" + ) + for (src in cases) { + println("== $src") + try { + println(PredParser(src).parse()) + } catch (e: Trauma.Predicate) { + println(" ! ${e.message}") + } + } + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt new file mode 100644 index 000000000..25c93f575 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt @@ -0,0 +1,17 @@ +package taboolib.module.incision.pred + +/** + * 编译后的谓词。 + * + * 由 [PredCompiler] 从 [PredAst] 生成 ASM 字节码并装载到目标 ClassLoader 后实例化。 + * + * 实现类要求: + * - 无状态、线程安全(dispatcher 多线程并发 `test`) + * - `test` 内部不允许抛异常逃出(除非是无法兜底的 [taboolib.module.incision.diagnostic.Trauma.Predicate.RuntimeFailure]) + * - `as T` 失败 → false + * - `?.` 在 null 上 → false + * - 普通成员访问失败若处于 `??` 兜底链则 false,否则交给上层(由 dispatcher warnOnce) + */ +interface Predicate { + fun test(ctx: EvalContext): Boolean +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/proxy/AdviceProxy.kt b/module/incision/src/main/kotlin/taboolib/module/incision/proxy/AdviceProxy.kt new file mode 100644 index 000000000..42e28bf82 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/proxy/AdviceProxy.kt @@ -0,0 +1,50 @@ +package taboolib.module.incision.proxy + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.runtime.AdviceEntry + +/** + * 跨 ClassLoader advice 代理 — 网关持有的"非类型化"接口对象。 + * + * 网关不能直接持有 [AdviceEntry](含 Kotlin 类型),跨 CL 时类型不一致。 + * 因此 [AdviceProxy] 仅暴露原始类型签名,由各插件的 incision 在自己 CL 上调用回去。 + */ +interface AdviceProxy { + + /** 调用 advice,返回值若非 null 则替换原方法返回值 */ + fun invoke(targetSig: String, self: Any?, args: Array): Any? + + /** 排序优先级 — 网关按此降序触发 */ + fun priority(): Int = 0 + + /** 注册者标识,用于按插件清理 */ + fun pluginName(): String = "" +} + +/** + * 在本 incision 实例内,把 [AdviceEntry] 包装为可跨 CL 注册的 [AdviceProxy]。 + */ +fun AdviceEntry.asProxy(pluginName: String = ""): AdviceProxy = object : AdviceProxy { + override fun invoke(targetSig: String, self: Any?, args: Array): Any? { + // 直接走本地 dispatcher 的链路 — 由 IncisionGate 调度 + return taboolib.module.incision.runtime.TheatreDispatcher.dispatch(targetSig, self, args) + } + override fun priority(): Int = this@asProxy.priority + override fun pluginName(): String = pluginName +} + +/** + * 跨 CL 字节码 weaver 代理 —— 网关 ensureImplanted 时调用。 + */ +interface BytecodeWeaverProxy { + + fun weave(originalBytes: ByteArray, target: MethodCoordinate): ByteArray +} + +/** + * 全局 suture 句柄 — 网关返回给注册方,用于 unregister。 + */ +data class GlobalSutureToken( + val target: MethodCoordinate, + val proxyRef: Any, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/reflex/IncisionReflex.kt b/module/incision/src/main/kotlin/taboolib/module/incision/reflex/IncisionReflex.kt new file mode 100644 index 000000000..1d1608608 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/reflex/IncisionReflex.kt @@ -0,0 +1,135 @@ +package taboolib.module.incision.reflex + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.runtime.AdviceEntry +import taboolib.module.incision.runtime.SurgeryRegistry +import taboolib.module.incision.runtime.TheatreDispatcher +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * Incision 反射协作工具 — 让 TabooLib 自带反射 (org.tabooproject.reflex) 与外部反射工具 + * 在面对被 incision 织入过的类/方法时,能拿到正确的"原始"语义,避免: + * + * 1. **递归触发**:反射调用某方法时再次触发 advice 链,造成栈溢出或语义偏差。 + * 2. **签名误判**:incision 在方法 HEAD/TAIL 注入 INVOKESTATIC,方法签名本身不改变; + * 但若上层工具按方法字节做 hash / 缓存,需要识别这是织入后的副本。 + * 3. **找不到字段/方法**:incision **不重命名** 现有成员、**不新增** public 方法, + * 所以 reflex 默认查找仍然成立。但用户可能希望"穿透代理"获取真正未拦截的实现。 + * + * 公开 API: + * - [withoutIncision]:以线程为单位临时禁用 dispatcher,确保块内反射调用不触发 advice。 + * - [invokeOriginal]:跳过整条 advice 链直调原方法(仍走原 invoke 字节码)。 + * - [isWoven]:判断方法当前是否被 incision 接管。 + * - [installReflexAdapter]:把适配器注入 `org.tabooproject.reflex.Reflex`, + * 使 TabooLib 反射在调用 method.invoke 时自动 wrap [withoutIncision]。 + */ +object IncisionReflex { + + /** 线程本地禁用标记 — TheatreDispatcher 在 dispatch 入口检查 */ + val suppressed: ThreadLocal = ThreadLocal.withInitial { 0 } + + /** + * 检查当前线程是否处于禁用 incision 的作用域中。 + * 由 [TheatreDispatcher] 在 dispatch 入口调用 — 若为 true,跳过整条 chain, + * 直接放行(返回 null)。 + */ + fun isCurrentlySuppressed(): Boolean = suppressed.get() > 0 + + /** + * 在 [block] 执行期间,对当前线程禁用 incision dispatcher。 + * 嵌套调用安全 — 用引用计数。 + */ + inline fun withoutIncision(block: () -> R): R { + suppressed.set(suppressed.get() + 1) + try { + return block() + } finally { + suppressed.set(suppressed.get() - 1) + } + } + + /** 反射 invoke 包装 — 与 [withoutIncision] 一致,但兼容 Java 调用方 */ + @JvmStatic + fun invokeOriginal(method: Method, instance: Any?, vararg args: Any?): Any? { + return withoutIncision { method.invoke(instance, *args) } + } + + @JvmStatic + fun getFieldOriginal(field: Field, instance: Any?): Any? { + return withoutIncision { field.get(instance) } + } + + @JvmStatic + fun setFieldOriginal(field: Field, instance: Any?, value: Any?) { + withoutIncision { field.set(instance, value) } + } + + /** 判断方法是否被 incision 接管 — 用于诊断 / 工具 */ + @JvmStatic + fun isWoven(method: Method): Boolean { + val owner = method.declaringClass.name.replace('.', '/') + val coord = MethodCoordinate(owner, method.name, descriptorOf(method)) + return SurgeryRegistry.listByTarget(coord).isNotEmpty() + } + + /** 列出某个类上所有 incision advice,便于调试 / `/incision list` 命令 */ + @JvmStatic + fun adviceOn(cls: Class<*>): Map> { + val owner = cls.name.replace('.', '/') + return SurgeryRegistry.list() + .flatMap { s -> s.targets.filter { it.owner == owner } } + .distinct() + .associateWith { TheatreDispatcher.chainOf(it).list() } + } + + /** + * 注册到 TabooLib 反射 (`org.tabooproject.reflex.Reflex`) 的钩子 — + * 让其默认在 method.invoke 包一层 [withoutIncision],避免反射触发 advice。 + * + * 实现方式:通过反射读取 Reflex 的全局静态适配器字段(若存在), + * 替换 / 链式包装;如果 Reflex 未提供该挂钩点,记录 warn 但不抛错。 + */ + fun installReflexAdapter() { + try { + val reflexCls = Class.forName("org.tabooproject.reflex.Reflex") + // Reflex 没有公开的 invoke hook,用 ReflexRemapper 通道虽然可以转译名字, + // 但拦不到 invoke 调用。做法:注册一个 thread-aware 的自定义 ClassLoader-level + // wrapper —— 当前能做的最优解是在 Reflex 调用前后由用户主动 withoutIncision。 + // + // 这里为后续 PR 留 hook:如果 Reflex 暴露 `setInvokeInterceptor`,自动接入。 + val setter = try { + reflexCls.getMethod("setInvokeInterceptor", java.util.function.Function::class.java) + } catch (_: Throwable) { null } + if (setter != null) { + val interceptor = java.util.function.Function { target -> + withoutIncision { target.run() } + } + setter.invoke(null, interceptor) + } + } catch (_: Throwable) { + // Reflex 不在 classpath 时跳过 + } + } + + private fun descriptorOf(m: Method): String { + val sb = StringBuilder("(") + for (p in m.parameterTypes) sb.append(typeDescriptor(p)) + sb.append(')').append(typeDescriptor(m.returnType)) + return sb.toString() + } + + private fun typeDescriptor(c: Class<*>): String = when { + c == Void.TYPE -> "V" + c == java.lang.Boolean.TYPE -> "Z" + c == java.lang.Byte.TYPE -> "B" + c == java.lang.Short.TYPE -> "S" + c == java.lang.Integer.TYPE -> "I" + c == java.lang.Long.TYPE -> "J" + c == java.lang.Float.TYPE -> "F" + c == java.lang.Double.TYPE -> "D" + c == java.lang.Character.TYPE -> "C" + c.isArray -> "[" + typeDescriptor(c.componentType) + else -> "L${c.name.replace('.', '/')};" + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/remap/NameResolver.kt b/module/incision/src/main/kotlin/taboolib/module/incision/remap/NameResolver.kt new file mode 100644 index 000000000..22b334a71 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/remap/NameResolver.kt @@ -0,0 +1,31 @@ +package taboolib.module.incision.remap + +/** + * 名称解析器 — 把用户写的目标名(可能是 Mojang 名、Spigot 名或版本化包名) + * 转换为当前运行环境下真实存在的类/方法/字段名。 + * + * - 默认实现 [NoopResolver] 对所有名字原样返回。 + * - 当 `module:bukkit-nms` 存在时,Bootstrap 会绑定 TabooLibNmsResolver + * 并对 `net/minecraft/`、`com/mojang/`、`org/bukkit/craftbukkit/` 等 owner + * 自动走 NMS 映射路径。 + */ +interface NameResolver { + + /** 把 internalName (斜杠形式) 规范化为当前运行环境下的 internalName */ + fun resolveOwner(internalName: String): String + + /** 返回 (规范化名, 规范化描述符) */ + fun resolveMethod(owner: String, name: String, desc: String): Pair + + /** 返回 (规范化名, 规范化描述符) */ + fun resolveField(owner: String, name: String, desc: String): Pair + + /** 是否需要重映射(Noop 对非 NMS 返回 false) */ + fun isRemappableTarget(owner: String): Boolean + + /** 进入缓存键的环境签名;环境变化时强制重译 */ + fun envSignature(): String + + /** 诊断用名称 */ + fun name(): String = javaClass.simpleName +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/remap/NoopResolver.kt b/module/incision/src/main/kotlin/taboolib/module/incision/remap/NoopResolver.kt new file mode 100644 index 000000000..6818d5cc0 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/remap/NoopResolver.kt @@ -0,0 +1,17 @@ +package taboolib.module.incision.remap + +/** + * 默认空实现 — 任何名字原样返回,用于非 NMS 场景。 + */ +object NoopResolver : NameResolver { + + override fun resolveOwner(internalName: String): String = internalName + + override fun resolveMethod(owner: String, name: String, desc: String): Pair = name to desc + + override fun resolveField(owner: String, name: String, desc: String): Pair = name to desc + + override fun isRemappableTarget(owner: String): Boolean = false + + override fun envSignature(): String = "noop" +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/remap/RemapRouter.kt b/module/incision/src/main/kotlin/taboolib/module/incision/remap/RemapRouter.kt new file mode 100644 index 000000000..632878443 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/remap/RemapRouter.kt @@ -0,0 +1,33 @@ +package taboolib.module.incision.remap + +/** + * 路由器 — 根据 owner 决定走 [NoopResolver] 还是 NMS resolver。 + * + * Bootstrap 阶段若检测到 `taboolib.module.nms.MinecraftVersion` 在 classpath, + * 会把 [nms] 字段设为 `TabooLibNmsResolver`,否则保留为 null 并一律走 Noop。 + */ +object RemapRouter : NameResolver { + + @Volatile + var nms: NameResolver? = null + + private fun pick(owner: String): NameResolver { + val r = nms + if (r != null && r.isRemappableTarget(owner)) return r + return NoopResolver + } + + override fun resolveOwner(internalName: String): String = pick(internalName).resolveOwner(internalName) + + override fun resolveMethod(owner: String, name: String, desc: String): Pair = + pick(owner).resolveMethod(owner, name, desc) + + override fun resolveField(owner: String, name: String, desc: String): Pair = + pick(owner).resolveField(owner, name, desc) + + override fun isRemappableTarget(owner: String): Boolean = nms?.isRemappableTarget(owner) == true + + override fun envSignature(): String = "router:${nms?.envSignature() ?: "noop-only"}" + + override fun name(): String = "RemapRouter[${nms?.name() ?: "noop"}]" +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/remap/TabooLibNmsResolver.kt b/module/incision/src/main/kotlin/taboolib/module/incision/remap/TabooLibNmsResolver.kt new file mode 100644 index 000000000..afee954d4 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/remap/TabooLibNmsResolver.kt @@ -0,0 +1,148 @@ +package taboolib.module.incision.remap + +import taboolib.module.incision.diagnostic.Forensics + +/** + * TabooLib NMS 绑定 — 通过反射 `taboolib.module.nms.MinecraftVersion` 与 `RemapTranslation` + * 实现名称转译。若任一反射失败,整个 resolver 回退为不可用。 + * + * 设计目的:使 incision 对 `:module:bukkit-nms` 保持"可选依赖"关系。 + */ +object TabooLibNmsResolver : NameResolver { + + private val minecraftVersionCls: Class<*>? = tryClass("taboolib.module.nms.MinecraftVersion") + private val remapTranslationCls: Class<*>? = tryClass("taboolib.module.nms.remap.RemapTranslation") + + @Volatile + private var delegate: Any? = null + + @Volatile + private var env: String = "unavailable" + + private fun tryClass(name: String): Class<*>? = try { + Class.forName(name, false, TabooLibNmsResolver::class.java.classLoader) + } catch (_: Throwable) { null } + + fun installIfAvailable(): Boolean { + val mvc = minecraftVersionCls ?: return false + val rtc = remapTranslationCls ?: return false + return try { + val instance = mvc.getField("INSTANCE").get(null) + val isUnobf = readBoolProp(mvc, instance, "isUnobfuscated") ?: false + val isUniCB = readBoolProp(mvc, instance, "isUniversalCraftBukkit") ?: false + val mcVer = readProp(mvc, instance, "minecraftVersion")?.toString() ?: "?" + val isMojang = readBoolProp(mvc, instance, "isMojangMapping") ?: false + + val implClassName = when { + isUnobf -> "taboolib.module.nms.remap.RemapTranslationUnobfsucated" + isUniCB -> "taboolib.module.nms.remap.RemapTranslationTabooLib" + else -> "taboolib.module.nms.remap.RemapTranslationLegacy" + } + val implCls = try { Class.forName(implClassName) } catch (_: Throwable) { rtc } + delegate = implCls.getDeclaredConstructor().newInstance() + env = "nms=$mcVer,unobf=$isUnobf,uniCB=$isUniCB,mojang=$isMojang,impl=${implCls.simpleName}" + RemapRouter.nms = this + Forensics.info("TabooLibNmsResolver 已启用: $env") + true + } catch (t: Throwable) { + Forensics.warn("TabooLibNmsResolver 初始化失败: ${t.message}") + false + } + } + + private fun readProp(cls: Class<*>, instance: Any, name: String): Any? { + // Kotlin object 属性:先尝试 getter 方法(isXxx / getXxx),再回退到字段 + for (getter in listOf(name, "get${name.replaceFirstChar { it.uppercase() }}", "is${name.replaceFirstChar { it.uppercase() }}")) { + try { return cls.getMethod(getter).invoke(instance) } catch (_: Throwable) {} + } + try { return cls.getField(name).get(instance) } catch (_: Throwable) {} + try { return cls.getDeclaredField(name).apply { isAccessible = true }.get(instance) } catch (_: Throwable) {} + return null + } + + private fun readBoolProp(cls: Class<*>, instance: Any, name: String): Boolean? = + readProp(cls, instance, name) as? Boolean + + override fun resolveOwner(internalName: String): String { + val d = delegate ?: return internalName + return try { + val m = d.javaClass.getMethod("translate", String::class.java) + val result = m.invoke(d, internalName) as String + if (result != internalName) { + Forensics.debug("resolveOwner: $internalName → $result") + } + result + } catch (t: Throwable) { + Forensics.warn("resolveOwner 失败: $internalName — ${t.javaClass.name}: ${t.message}") + internalName + } + } + + override fun resolveMethod(owner: String, name: String, desc: String): Pair { + val d = delegate ?: return name to desc + return try { + // 走 owner 的实际类名(已转译)调用 Remapper.mapMethodName(owner, name, descriptor) + val resolvedOwner = resolveOwner(owner) + val m = mapMethodNameMethod(d) ?: return name to desc + val mapped = m.invoke(d, resolvedOwner, name, desc) as? String ?: name + if (mapped != name) { + Forensics.debug("resolveMethod: $owner.$name$desc → $mapped") + } + mapped to desc + } catch (t: Throwable) { + Forensics.warn("resolveMethod 失败: $owner.$name$desc — ${t.javaClass.name}: ${t.message}") + name to desc + } + } + + override fun resolveField(owner: String, name: String, desc: String): Pair { + val d = delegate ?: return name to desc + return try { + val resolvedOwner = resolveOwner(owner) + val m = mapFieldNameMethod(d) ?: return name to desc + val mapped = m.invoke(d, resolvedOwner, name, desc) as? String ?: name + if (mapped != name) { + Forensics.debug("resolveField: $owner.$name:$desc → $mapped") + } + mapped to desc + } catch (t: Throwable) { + Forensics.warn("resolveField 失败: $owner.$name:$desc — ${t.javaClass.name}: ${t.message}") + name to desc + } + } + + @Volatile private var cachedMapMethodName: java.lang.reflect.Method? = null + @Volatile private var cachedMapFieldName: java.lang.reflect.Method? = null + + private fun mapMethodNameMethod(d: Any): java.lang.reflect.Method? { + cachedMapMethodName?.let { return it } + return try { + // org.objectweb.asm.commons.Remapper#mapMethodName(String, String, String): String + val m = d.javaClass.getMethod("mapMethodName", String::class.java, String::class.java, String::class.java) + m.isAccessible = true + cachedMapMethodName = m + m + } catch (_: Throwable) { null } + } + + private fun mapFieldNameMethod(d: Any): java.lang.reflect.Method? { + cachedMapFieldName?.let { return it } + return try { + val m = d.javaClass.getMethod("mapFieldName", String::class.java, String::class.java, String::class.java) + m.isAccessible = true + cachedMapFieldName = m + m + } catch (_: Throwable) { null } + } + + override fun isRemappableTarget(owner: String): Boolean { + if (delegate == null) return false + return owner.startsWith("net/minecraft/") || + owner.startsWith("com/mojang/") || + owner.startsWith("org/bukkit/craftbukkit/") + } + + override fun envSignature(): String = "nms:$env" + + override fun name(): String = "TabooLibNmsResolver" +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt new file mode 100644 index 000000000..12599c64b --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/AdviceChain.kt @@ -0,0 +1,68 @@ +package taboolib.module.incision.runtime + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Theatre +import taboolib.module.incision.pred.Predicate +import taboolib.module.incision.weaver.site.SiteSpec + +/** + * Advice 类型。 + */ +enum class AdviceKind { LEAD, TRAIL, SPLICE, GRAFT, BYPASS, TRIM, EXCISE } + +/** + * 运行时单条 advice 记录 — dispatcher 表中的条目。 + */ +data class AdviceEntry( + val id: String, + val kind: AdviceKind, + val target: MethodCoordinate, + val priority: Int, + val handler: (Theatre) -> Any?, + val predicate: ((Theatre) -> Boolean)? = null, + /** + * 编译后的字符串谓词(来自 DSL 的 `where("...")` 字面量)。 + * 与 [predicate] 互不影响:dispatcher 先 test 此字段,再 test 闭包 [predicate]。 + * 解析与编译在 advice 注册期完成;运行期零反射、零 AST 遍历。 + */ + val compiledPredicate: Predicate? = null, + /** [compiledPredicate] 的源码,仅用于诊断输出。 */ + val predicateSource: String? = null, + val pluginName: String = "", + val classLoader: java.lang.ref.WeakReference? = null, + val explicitResumeRequired: Boolean = false, + val sourceKind: String = "dsl", + val aliasGroup: String? = null, + val siteSpec: SiteSpec? = null, + /** TRAIL 是否在异常出口 (ATHROW) 触发;默认 true。对非 TRAIL 无效。 */ + val onThrow: Boolean = true, + @Volatile var enabled: Boolean = true, +) + +/** + * 单个目标方法的 advice 链 — 按优先级降序,同优先级按注册顺序。 + */ +class AdviceChain(val target: MethodCoordinate) { + + private val entries = java.util.concurrent.CopyOnWriteArrayList() + + fun add(entry: AdviceEntry) { + entries.add(entry) + entries.sortByDescending { it.priority } + } + + fun remove(id: String): Boolean = entries.removeIf { it.id == id } + + fun removeByClassLoader(cl: ClassLoader): Int { + var n = 0 + entries.removeIf { e -> + val held = e.classLoader?.get() + if (held === cl) { n++; true } else false + } + return n + } + + fun list(): List = entries.toList() + + fun isEmpty(): Boolean = entries.isEmpty() +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/DispatchSignatureCodec.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/DispatchSignatureCodec.kt new file mode 100644 index 000000000..fa952a26d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/DispatchSignatureCodec.kt @@ -0,0 +1,63 @@ +package taboolib.module.incision.runtime + +import java.util.Base64 + +/** + * dispatch 签名协议编解码。 + * + * 约定格式: + * - phase 入口:`baseSig@PHASE` + * - site 入口:`baseSig#encodedAdviceId` + * - phase + site 不叠加;site 侧用 adviceId 精确路由到单条 entry + * + * adviceId 本身可能含 `@` / `#`,因此统一做 URL-safe Base64 编码并加前缀, + * 避免被 phase / id 分隔逻辑误拆。 + */ +object DispatchSignatureCodec { + + private const val advicePrefix = "b64:" + private val phaseSuffixes = setOf("LEAD", "TRAIL", "SPLICE", "TRAIL_THROW") + + data class Parsed( + val baseSig: String, + val adviceId: String?, + val phase: String?, + ) + + fun compose(baseSig: String, adviceId: String): String { + if (adviceId.isBlank()) return baseSig + return "$baseSig#${encodeAdviceId(adviceId)}" + } + + fun parse(targetSig: String): Parsed { + val (sigWithoutPhase, phase) = splitPhase(targetSig) + val hashIdx = sigWithoutPhase.indexOf('#') + if (hashIdx < 0) return Parsed(sigWithoutPhase, null, phase) + val baseSig = sigWithoutPhase.substring(0, hashIdx) + val encodedAdviceId = sigWithoutPhase.substring(hashIdx + 1).takeIf { it.isNotEmpty() } + return Parsed(baseSig, encodedAdviceId?.let(::decodeAdviceId), phase) + } + + private fun splitPhase(targetSig: String): Pair { + val atIdx = targetSig.lastIndexOf('@') + if (atIdx <= 0) return targetSig to null + val maybePhase = targetSig.substring(atIdx + 1) + return if (maybePhase in phaseSuffixes) { + targetSig.substring(0, atIdx) to maybePhase + } else { + targetSig to null + } + } + + private fun encodeAdviceId(adviceId: String): String { + val encoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(adviceId.toByteArray(Charsets.UTF_8)) + return advicePrefix + encoded + } + + private fun decodeAdviceId(encodedAdviceId: String): String { + if (!encodedAdviceId.startsWith(advicePrefix)) return encodedAdviceId + val encoded = encodedAdviceId.removePrefix(advicePrefix) + return String(Base64.getUrlDecoder().decode(encoded), Charsets.UTF_8) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/SurgeryRegistry.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/SurgeryRegistry.kt new file mode 100644 index 000000000..04fac50c5 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/SurgeryRegistry.kt @@ -0,0 +1,53 @@ +package taboolib.module.incision.runtime + +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Suture +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import java.util.concurrent.ConcurrentHashMap + +/** + * 全局切术注册表。 + * + * 持有 id → Suture 的映射,负责: + * - 唯一性校验(DuplicateId) + * - 按 holder / target / plugin 查询 + * - 卸载时从 dispatcher chain 中移除 + * + * 同一 JVM 内有且仅有一个实例(本地层面;跨插件的全局协调由 IncisionGate 负责)。 + */ +object SurgeryRegistry { + + private val byId = ConcurrentHashMap() + private val byTarget = ConcurrentHashMap>() + + fun register(id: String, suture: Suture) { + val prev = byId.putIfAbsent(id, suture) + if (prev != null) { + throw Trauma.Declaration.DuplicateId(id, prev.javaClass.name) + } + for (t in suture.targets) { + byTarget.computeIfAbsent(t.signature) { mutableListOf() }.add(suture) + } + Forensics.debug("register id=$id targets=${suture.targets}") + } + + fun unregister(id: String): Boolean { + val s = byId.remove(id) ?: return false + for (t in s.targets) { + byTarget[t.signature]?.remove(s) + } + return true + } + + fun find(id: String): Suture? = byId[id] + + fun list(): Collection = byId.values + + fun listByTarget(target: MethodCoordinate): List = byTarget[target.signature].orEmpty().toList() + + fun clear() { + byId.clear() + byTarget.clear() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt new file mode 100644 index 000000000..da52a493b --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/runtime/TheatreDispatcher.kt @@ -0,0 +1,383 @@ +package taboolib.module.incision.runtime + +import io.izzel.incision.bridge.IncisionBridge +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Resume +import taboolib.module.incision.api.Theatre +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.pred.EvalContextImpl +import taboolib.module.incision.weaver.BodiesClassGenerator +import taboolib.module.incision.weaver.BridgeClassLoader +import java.util.concurrent.ConcurrentHashMap + +/** + * 手术现场调度器 — 接收织入字节码的回调,按 advice 链顺序触发 handler。 + * + * 由于实际部署时调度入口是 IncisionGate(系统 ClassLoader 持有), + * 本类是"本地实现",会被网关在最终代理时复用 / 包装。 + */ +object TheatreDispatcher { + + @JvmField + val BYPASS_MISS: Any = IncisionBridge.bypassMiss() + + private val chains = ConcurrentHashMap() + private val wildcardEntries = ConcurrentHashMap>() + + /** -Dincision.predicate.strict=true 时,谓词运行期异常上抛而非吞掉。 */ + private val predicateStrict: Boolean = + System.getProperty("incision.predicate.strict", "false").toBoolean() + + /** 谓词异常 warn 去重表:advice id → 已 warn 标记。 */ + private val predicateWarnedIds = ConcurrentHashMap() + + private inline fun warnOnce(id: String, block: () -> Unit) { + if (predicateWarnedIds.putIfAbsent(id, true) == null) block() + } + + // bodies 方法缓存:baseSig → Method(未命中时缓存为 null 的 Optional 包装) + private val bodyInvokerCache = ConcurrentHashMap>() + + /** + * 从 BridgeClassLoader 中查找 bodies 类的对应方法。 + * + * @param baseSig 格式如 "com/example/Foo.greet(Ljava/lang/String;I)I" + * @return bodies 类中的 static Method,或 null + */ + fun resolveBodyInvoker(baseSig: String): java.lang.reflect.Method? { + val cached = bodyInvokerCache[baseSig] + if (cached != null) return cached.orElse(null) + val resolved: java.lang.reflect.Method? = try { + val parsed = parseOwnerAndMethod(baseSig) + if (parsed == null) { + null + } else { + val bodiesClassName = BridgeClassLoader.bodiesClassName(parsed.ownerInternal) + if (!BridgeClassLoader.INSTANCE.hasBodies(bodiesClassName)) { + null + } else { + val bodiesClass = BridgeClassLoader.INSTANCE.loadClass(bodiesClassName) + val bodyMethodName = parsed.methodName + BodiesClassGenerator.BODY_METHOD_SUFFIX + bodiesClass.getMethod(bodyMethodName, Any::class.java, Array::class.java) + } + } + } catch (t: Throwable) { + Forensics.debug("resolveBodyInvoker 未命中: $baseSig — ${t.message}") + null + } + bodyInvokerCache[baseSig] = java.util.Optional.ofNullable(resolved) + return resolved + } + + private data class ParsedSig(val ownerInternal: String, val methodName: String, val desc: String) + + /** + * 从 baseSig 解析 owner/name/desc。 + * 格式: + * "com/example/Foo.greet(Ljava/lang/String;I)I" + */ + private fun parseOwnerAndMethod(baseSig: String): ParsedSig? { + val dotIdx = baseSig.indexOf('.') + if (dotIdx < 0) return null + val owner = baseSig.substring(0, dotIdx) + val rest = baseSig.substring(dotIdx + 1) + val parenIdx = rest.indexOf('(') + if (parenIdx < 0) return null + val name = rest.substring(0, parenIdx) + val desc = rest.substring(parenIdx) + return ParsedSig(owner, name, desc) + } + + fun chainOf(target: MethodCoordinate): AdviceChain = + chains.computeIfAbsent(target.signature) { AdviceChain(target) } + + fun register(entry: AdviceEntry) { + val desc = entry.target.descriptor + if (desc.contains('*')) { + val ownerName = "${entry.target.owner}.${entry.target.name}" + wildcardEntries.computeIfAbsent(ownerName) { java.util.concurrent.CopyOnWriteArrayList() }.add(entry) + // 也在 chains 中创建通配签名对应的 chain,保证 weaver 用通配签名 LDC 时能命中 + chainOf(entry.target).add(entry) + for ((sig, chain) in chains) { + if (sig == entry.target.signature) continue + if (chain.target.owner == entry.target.owner && chain.target.name == entry.target.name) { + chain.add(entry) + } + } + } else { + val chain = chainOf(entry.target) + chain.add(entry) + val ownerName = "${entry.target.owner}.${entry.target.name}" + wildcardEntries[ownerName]?.forEach { w -> chain.add(w) } + } + } + + fun unregister(target: MethodCoordinate, id: String): Boolean = + chains[target.signature]?.remove(id) ?: false + + /** + * 织入字节码的回调入口。返回值用作目标方法的返回值(若被覆盖)。 + * + * @param targetSig 编译期常量字符串,格式 `owner.name(desc)return` + * @param self 实例(静态方法时为 null) + * @param args 原方法实参 + * @param originalInvoker 调用原方法的桥(由 weaver 注入;splice 链尾会用到) + */ + @JvmStatic + fun dispatch( + targetSig: String, + self: Any?, + args: Array, + originalInvoker: ((Array) -> Any?)? = null, + ): Any? { + if (taboolib.module.incision.reflex.IncisionReflex.isCurrentlySuppressed()) { + return originalInvoker?.invoke(args) + } + val parsedSig = DispatchSignatureCodec.parse(targetSig) + val baseSig = parsedSig.baseSig + val adviceId = parsedSig.adviceId + val phase = parsedSig.phase + val chain = chains[baseSig] + if (chain == null) return originalInvoker?.invoke(args) + val isThrowPhase = phase == "TRAIL_THROW" + val entries = chain.list().filter { e -> + if (!e.enabled) return@filter false + if (adviceId != null) return@filter e.id == adviceId + if (phase == null) return@filter true + if (isThrowPhase) return@filter e.kind == AdviceKind.TRAIL && e.onThrow + matchesPhase(e, phase) + } + if (entries.isEmpty()) return originalInvoker?.invoke(args) + + // SPLICE 相位下若 weaver 未注入 invoker,尝试从 bodies side-car 解析 + val effectiveInvoker: ((Array) -> Any?)? = originalInvoker ?: run { + if (phase == "SPLICE") { + // 确保 BridgeClassLoader 能解析目标类:用 self 的 CL 注册为 delegate + self?.javaClass?.classLoader?.let { BridgeClassLoader.INSTANCE.registerDelegate(it) } + val bodyMethod = resolveBodyInvoker(baseSig) + if (bodyMethod != null) { + { callArgs -> bodyMethod.invoke(null, self, callArgs) } + } else null + } else null + } + + val target = chain.target + // TRAIL_THROW 相位:args[0] 是织入期传入的 Throwable;ctx.throwable 回填 + val (ctxArgs, pendingThrow) = if (isThrowPhase) { + val thr = args.firstOrNull() as? Throwable + emptyArray() to thr + } else args to null + val ctx = TheatreImpl(target, self, ctxArgs, entries, effectiveInvoker) + if (pendingThrow != null) ctx.setPendingThrowable(pendingThrow) + return ctx.runChain() + } + + @JvmStatic + fun dispatchBypass(targetSig: String, self: Any?, args: Array): Any? { + if (taboolib.module.incision.reflex.IncisionReflex.isCurrentlySuppressed()) { + return BYPASS_MISS + } + val parsedSig = DispatchSignatureCodec.parse(targetSig) + val baseSig = parsedSig.baseSig + val adviceId = parsedSig.adviceId + val phase = parsedSig.phase + val chain = chains[baseSig] ?: return BYPASS_MISS + val entries = chain.list().filter { e -> + if (!e.enabled) return@filter false + if (adviceId != null) return@filter e.id == adviceId + if (phase == null) return@filter e.kind == AdviceKind.BYPASS + matchesPhase(e, phase) + } + if (entries.isEmpty()) return BYPASS_MISS + val ctx = TheatreImpl(chain.target, self, args, entries, null) + val result = ctx.runChain() + return if (ctx.wasIntercepted()) result else BYPASS_MISS + } + + private fun matchesPhase(entry: AdviceEntry, phase: String): Boolean = when (phase) { + "LEAD" -> entry.kind == AdviceKind.LEAD + "TRAIL" -> entry.kind == AdviceKind.TRAIL + "SPLICE" -> entry.kind == AdviceKind.SPLICE || + entry.kind == AdviceKind.EXCISE || + (entry.kind == AdviceKind.BYPASS && entry.siteSpec == null) + else -> true + } + + fun clear() { chains.clear(); wildcardEntries.clear() } + + /** 内部 Theatre + Resume 实现 — 链式驱动 */ + private class TheatreImpl( + override val target: MethodCoordinate, + override val self: Any?, + override val args: Array, + val entries: List, + val originalInvoker: ((Array) -> Any?)?, + ) : Theatre, Resume { + + override val resume: Resume get() = this + override var throwable: Throwable? = null + private set + + internal fun setPendingThrowable(t: Throwable) { throwable = t } + + private var idx = 0 + private var skipResult: Any? = null + private var skipped = false + private var explicitControlUsed = false + private var hasProceeded = false + private var resultAvailable = false + private var currentResult: Any? = UNSET + + override val proceeded: Boolean get() = hasProceeded + + fun runChain(): Any? { + // 注意:不要走 proceedInternal —— 它会预置 explicitControlUsed/hasProceeded=true, + // 导致 handleSpliceLike 无法识别"用户在 handler 内是否调用过 proceed()", + // 进而对所有 @Splice 抛出 ResumeMissing。runChain 只负责进入链,不算用户显式放行。 + val result = continueChain(useExistingResult = false) + return if (skipped) skipResult else result + } + + /** + * 执行 advice 上挂载的字符串编译谓词。失败: + * - 默认:warnOnce + 视为 true(不阻断)。这样运行期谓词异常不会让 advice 静默失活。 + * - `-Dincision.predicate.strict=true`:抛 [Trauma.Predicate.RuntimeFailure]。 + */ + private fun evalCompiledPredicate(cur: AdviceEntry): Boolean { + val pred = cur.compiledPredicate ?: return true + val ctx = EvalContextImpl(this, target, resultProvider = { + if (resultAvailable && currentResult !== UNSET) currentResult else null + }) + return try { + pred.test(ctx) + } catch (t: Throwable) { + if (predicateStrict) { + throw Trauma.Predicate.RuntimeFailure(cur.predicateSource ?: "", cur.id, t) + } + warnOnce(cur.id) { + Forensics.warn( + "[predicate] advice=${cur.id} test 抛异常被忽略(strict=false): ${t.message}\n" + + " source: ${cur.predicateSource ?: ""}" + ) + } + true + } + } + + override fun proceed(): Any? = proceedInternal(args, useExistingResult = resultAvailable) + + override fun proceed(vararg args: Any?): Any? = proceedInternal(args, useExistingResult = false) + + override fun proceedResult(returnValue: Any?): Any? { + explicitControlUsed = true + resultAvailable = true + currentResult = returnValue + return proceedInternal(args, useExistingResult = true) + } + + override fun skip(returnValue: Any?): Any? { + explicitControlUsed = true + skipResult = returnValue + skipped = true + return returnValue + } + + private fun proceedInternal(callArgs: Array, useExistingResult: Boolean): Any? { + if (skipped) return skipResult + applyArgs(callArgs) + explicitControlUsed = true + hasProceeded = true + return continueChain(useExistingResult) + } + + private fun continueChain(useExistingResult: Boolean): Any? { + if (skipped) return skipResult + if (idx >= entries.size) { + return finalizeLeaf(useExistingResult) + } + val cur = entries[idx++] + if (!evalCompiledPredicate(cur)) { + return continueChain(useExistingResult) + } + if (cur.predicate != null && !cur.predicate.invoke(this)) { + return continueChain(useExistingResult) + } + return try { + when (cur.kind) { + AdviceKind.LEAD -> handleLead(cur) + AdviceKind.TRAIL -> handleTrail(cur) + AdviceKind.SPLICE, AdviceKind.GRAFT, AdviceKind.BYPASS, + AdviceKind.TRIM, AdviceKind.EXCISE -> handleSpliceLike(cur) + } + } catch (t: Throwable) { + Forensics.report(Trauma.Runtime.HandlerThrew(cur.id, target, t)) + throw t + } + } + + private fun handleLead(cur: AdviceEntry): Any? { + cur.handler(this) + if (skipped) return skipResult + return continueChain(useExistingResult = resultAvailable) + } + + private fun handleTrail(cur: AdviceEntry): Any? { + val downstream = continueChain(useExistingResult = resultAvailable) + if (skipped) return skipResult + if (!resultAvailable) { + currentResult = downstream + resultAvailable = true + } + cur.handler(this) + return if (skipped) skipResult else currentResolvedResult() + } + + private fun handleSpliceLike(cur: AdviceEntry): Any? { + val explicitBefore = explicitControlUsed + val idxBefore = idx + val resultAvailableBefore = resultAvailable + val currentResultBefore = currentResult + val result = cur.handler(this) + if (skipped) return skipResult + if (explicitControlUsed != explicitBefore || idx != idxBefore) { + return if (skipped) skipResult else currentResolvedResult() + } + if (result != null) { + return skip(result) + } + if (cur.explicitResumeRequired) { + throw Trauma.Runtime.ResumeMissing(cur.id, target) + } + resultAvailable = resultAvailableBefore + currentResult = currentResultBefore + return continueChain(useExistingResult = resultAvailable) + } + + private fun finalizeLeaf(useExistingResult: Boolean): Any? { + if (skipped) return skipResult + if (useExistingResult && resultAvailable && currentResult !== UNSET) { + return currentResolvedResult() + } + val invoked = originalInvoker?.invoke(this.args) + currentResult = invoked + resultAvailable = true + return invoked + } + + private fun currentResolvedResult(): Any? = if (resultAvailable && currentResult !== UNSET) currentResult else null + + internal fun wasIntercepted(): Boolean = skipped || explicitControlUsed + + private fun applyArgs(callArgs: Array) { + val limit = minOf(callArgs.size, args.size) + for (i in 0 until limit) { + args[i] = callArgs[i] + } + } + + companion object { + private val UNSET = Any() + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/scope/ScopeParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/scope/ScopeParser.kt new file mode 100644 index 000000000..d9d6a817d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/scope/ScopeParser.kt @@ -0,0 +1,204 @@ +package taboolib.module.incision.scope + +import taboolib.module.incision.diagnostic.Trauma + +/** + * Scope DSL 语法树。 + * + * 语法: + * - `class:com.foo.Bar` 匹配类(支持 `*` 通配) + * - `method:Foo#bar(*)` 匹配方法(`*` 通配描述符段) + * - `field:Foo#name:String` 匹配字段 + * - `&` 与, `|` 或, `!` 非, `(...)` 分组 + */ +sealed class ScopeNode { + abstract fun matches(target: ScopeTarget): Boolean + + class And(val left: ScopeNode, val right: ScopeNode) : ScopeNode() { + override fun matches(t: ScopeTarget) = left.matches(t) && right.matches(t) + } + class Or(val left: ScopeNode, val right: ScopeNode) : ScopeNode() { + override fun matches(t: ScopeTarget) = left.matches(t) || right.matches(t) + } + class Not(val inner: ScopeNode) : ScopeNode() { + override fun matches(t: ScopeTarget) = !inner.matches(t) + } + class ClassMatch(val pattern: String) : ScopeNode() { + private val regex = patternToRegex(pattern) + override fun matches(t: ScopeTarget) = regex.matches(t.className) + } + class MethodMatch(val owner: String, val name: String, val descPattern: String) : ScopeNode() { + private val ownerRegex = patternToRegex(owner) + private val nameRegex = patternToRegex(name) + private val descRegex = patternToRegex(descPattern) + override fun matches(t: ScopeTarget) = + t.kind == ScopeTarget.Kind.METHOD && + ownerRegex.matches(t.className) && + nameRegex.matches(t.memberName ?: "") && + descRegex.matches(t.descriptor ?: "") + } + class FieldMatch(val owner: String, val name: String, val typePattern: String) : ScopeNode() { + private val ownerRegex = patternToRegex(owner) + private val nameRegex = patternToRegex(name) + private val typeRegex = patternToRegex(typePattern) + override fun matches(t: ScopeTarget) = + t.kind == ScopeTarget.Kind.FIELD && + ownerRegex.matches(t.className) && + nameRegex.matches(t.memberName ?: "") && + typeRegex.matches(t.descriptor ?: "") + } + + companion object { + internal fun patternToRegex(p: String): Regex { + val sb = StringBuilder() + for (ch in p) when (ch) { + '*' -> sb.append(".*") + '?' -> sb.append('.') + '.', '$', '(', ')', '[', ']', '+', '^' -> sb.append('\\').append(ch) + else -> sb.append(ch) + } + return Regex(sb.toString()) + } + } +} + +/** ScopeNode.matches 的输入 — 描述某个候选目标 */ +data class ScopeTarget( + val kind: Kind, + val className: String, + val memberName: String? = null, + val descriptor: String? = null, +) { + enum class Kind { CLASS, METHOD, FIELD } +} + +/** + * 词法 + 语法分析。 + * + * 出错时抛 [Trauma.Declaration.BadScope],附带原文与定位。 + */ +object ScopeParser { + + fun parse(raw: String): ScopeNode { + val tokens = tokenize(raw) + val parser = Parser(raw, tokens) + val node = parser.parseExpr() + if (parser.cursor < tokens.size) { + throw Trauma.Declaration.BadScope(raw, parser.posOf(parser.cursor), "意外的 token: ${tokens[parser.cursor]}") + } + return node + } + + private sealed class Tok(val pos: Int) { + class Atom(pos: Int, val text: String) : Tok(pos) + class And(pos: Int) : Tok(pos) + class Or(pos: Int) : Tok(pos) + class Not(pos: Int) : Tok(pos) + class LParen(pos: Int) : Tok(pos) + class RParen(pos: Int) : Tok(pos) + } + + private fun tokenize(raw: String): List { + val out = mutableListOf() + var i = 0 + while (i < raw.length) { + val c = raw[i] + when { + c.isWhitespace() -> { i++ } + c == '&' -> { out += Tok.And(i); i++ } + c == '|' -> { out += Tok.Or(i); i++ } + c == '!' -> { out += Tok.Not(i); i++ } + c == '(' -> { out += Tok.LParen(i); i++ } + c == ')' -> { out += Tok.RParen(i); i++ } + else -> { + val start = i + while (i < raw.length && raw[i] !in "&|!() \t") i++ + out += Tok.Atom(start, raw.substring(start, i)) + } + } + } + return out + } + + private class Parser(val raw: String, val tokens: List) { + var cursor = 0 + fun posOf(idx: Int) = if (idx < tokens.size) tokens[idx].pos else raw.length + + fun parseExpr(): ScopeNode = parseOr() + + fun parseOr(): ScopeNode { + var left = parseAnd() + while (cursor < tokens.size && tokens[cursor] is Tok.Or) { + cursor++ + left = ScopeNode.Or(left, parseAnd()) + } + return left + } + fun parseAnd(): ScopeNode { + var left = parseNot() + while (cursor < tokens.size && tokens[cursor] is Tok.And) { + cursor++ + left = ScopeNode.And(left, parseNot()) + } + return left + } + fun parseNot(): ScopeNode { + if (cursor < tokens.size && tokens[cursor] is Tok.Not) { + cursor++ + return ScopeNode.Not(parseAtom()) + } + return parseAtom() + } + fun parseAtom(): ScopeNode { + if (cursor >= tokens.size) { + throw Trauma.Declaration.BadScope(raw, raw.length, "意外结尾") + } + return when (val t = tokens[cursor]) { + is Tok.LParen -> { + cursor++ + val n = parseExpr() + if (cursor >= tokens.size || tokens[cursor] !is Tok.RParen) { + throw Trauma.Declaration.BadScope(raw, posOf(cursor), "缺少 ')'") + } + cursor++ + n + } + is Tok.Atom -> { + cursor++ + parseAtomText(t.text, t.pos) + } + else -> throw Trauma.Declaration.BadScope(raw, t.pos, "意外 token: $t") + } + } + fun parseAtomText(text: String, pos: Int): ScopeNode { + val colon = text.indexOf(':') + if (colon < 0) throw Trauma.Declaration.BadScope(raw, pos, "缺少 'class:' / 'method:' / 'field:' 前缀: '$text'") + val kind = text.substring(0, colon) + val body = text.substring(colon + 1) + return when (kind) { + "class" -> ScopeNode.ClassMatch(body) + "method" -> { + val hash = body.indexOf('#') + if (hash < 0) throw Trauma.Declaration.BadScope(raw, pos, "method 缺少 '#': '$body'") + val owner = body.substring(0, hash) + val rest = body.substring(hash + 1) + val paren = rest.indexOf('(') + val name = if (paren < 0) rest else rest.substring(0, paren) + val desc = if (paren < 0) "*" else rest.substring(paren) + ScopeNode.MethodMatch(owner, name, desc) + } + "field" -> { + val hash = body.indexOf('#') + if (hash < 0) throw Trauma.Declaration.BadScope(raw, pos, "field 缺少 '#': '$body'") + val owner = body.substring(0, hash) + val rest = body.substring(hash + 1) + val typeIdx = rest.indexOf(':') + val name = if (typeIdx < 0) rest else rest.substring(0, typeIdx) + val type = if (typeIdx < 0) "*" else rest.substring(typeIdx + 1) + ScopeNode.FieldMatch(owner, name, type) + } + else -> throw Trauma.Declaration.BadScope(raw, pos, "未知前缀 '$kind',期望 class/method/field") + } + } + } +} 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 new file mode 100644 index 000000000..b8cdf9ae0 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt @@ -0,0 +1,397 @@ +package taboolib.module.incision.weaver + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.IincInsnNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.LabelNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.TryCatchBlockNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode +import taboolib.module.incision.diagnostic.Forensics + +/** + * Side-car bodies 类生成器。 + * + * 对于目标类 `Foo`,当其方法被 SPLICE 织入(原方法体被 wrapper 替换)后, + * 本生成器将原方法体的指令流复制到一个同包伴生类 `Foo$$IncisionBodies` 的 + * static 方法中,签名统一为: + * + * ``` + * static Object $body(Object self, Object[] args) + * ``` + * + * 这样 [taboolib.module.incision.runtime] 的 `proceed()` / `proceedResult()` + * 可以通过反射调用 bodies 方法,获取原方法逻辑的执行结果。 + * + * 实现要点: + * - 接收**织入前**的原始字节码,复制指令流安全无递归 + * - slot 0/1 保留给 self/args 入参;slot 2 保存 CAST 后的 `_self`; + * slot 3.. 保存拆箱后的参数;原方法指令中所有 local slot 统一偏移 +2 + * - 返回指令全部替换为装箱 + ARETURN + * - try-catch 表通过 label map 一并映射 + * - COMPUTE_FRAMES 自动重算栈帧,不处理原 FrameNode + */ +object BodiesClassGenerator { + + const val BODIES_SUFFIX = "\$\$IncisionBodies" + const val BODY_METHOD_SUFFIX = "\$body" + + /** 统一的 body 方法描述符 */ + const val BODY_DESC = "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;" + + /** + * 生成 side-car bodies 类。 + * + * @param originalBytes 目标类的原始字节码(织入前) + * @param ownerInternalName 目标类 JVM 内部名,如 "com/example/Foo" + * @param targetMethods 需要生成 body 的方法集合,元素为 (name, descriptor) + * @return bodies 类字节码;若无任何方法可生成则返回 null + */ + fun generate( + originalBytes: ByteArray, + ownerInternalName: String, + targetMethods: Set>, + ): ByteArray? { + if (targetMethods.isEmpty()) return null + + // 读入原类 + val reader = ClassReader(originalBytes) + val originalNode = ClassNode() + reader.accept(originalNode, ClassReader.EXPAND_FRAMES) + + // 构造 bodies 类 + val bodiesNode = ClassNode() + bodiesNode.version = originalNode.version + bodiesNode.access = ACC_PUBLIC or ACC_SYNTHETIC + bodiesNode.name = ownerInternalName + BODIES_SUFFIX + bodiesNode.superName = "java/lang/Object" + + // 收集 private 字段 → 用于将 GETFIELD/PUTFIELD 替换为 getter/setter + val privateFields = originalNode.fields + ?.filter { it.access and ACC_PRIVATE != 0 } + ?.associate { it.name to it.desc } + ?: emptyMap() + + var generated = 0 + for (method in originalNode.methods) { + val key = method.name to method.desc + if (key !in targetMethods) continue + if (method.access and ACC_ABSTRACT != 0) continue + if (method.access and ACC_NATIVE != 0) continue + + val bodyMethod = generateBodyMethod(method, ownerInternalName, privateFields) + if (bodyMethod != null) { + bodiesNode.methods.add(bodyMethod) + generated++ + } + } + + if (generated == 0) return null + + val writer = object : ClassWriter(COMPUTE_MAXS or COMPUTE_FRAMES) { + override fun getCommonSuperClass(type1: String, type2: String): String { + // 保守但安全:frame 校验对象类型时统一退化到 Object + return "java/lang/Object" + } + } + bodiesNode.accept(writer) + return writer.toByteArray() + } + + // ───────────────────────────────────────────────────────────────── + + private fun generateBodyMethod(original: MethodNode, ownerInternal: String, privateFields: Map): MethodNode? { + if (original.access and ACC_STATIC != 0) return null + + val argTypes = Type.getArgumentTypes(original.desc) + val returnType = Type.getReturnType(original.desc) + + val body = MethodNode( + ACC_PUBLIC or ACC_STATIC, + original.name + BODY_METHOD_SUFFIX, + BODY_DESC, + null, + original.exceptions?.toTypedArray() + ) + + // 预检:不支持 INVOKESPECIAL 调用 owner 自身 private/实例方法(除 ) + if (hasUnsupportedInvokeSpecial(original, ownerInternal)) { + Forensics.debug( + "BodiesClassGenerator: 跳过 ${ownerInternal}.${original.name}${original.desc} " + + "(包含 INVOKESPECIAL 到自身方法,static 上下文不合法)" + ) + return null + } + + val insns = body.instructions + + // Prologue: (Object self, Object[] args) → _self + 拆箱后的参数 + // slot 0 = self, slot 1 = args, slot 2 = _self(owner), slot 3.. = unboxed args + insns.add(VarInsnNode(ALOAD, 0)) + insns.add(TypeInsnNode(CHECKCAST, ownerInternal)) + insns.add(VarInsnNode(ASTORE, 2)) + + var nextSlot = 3 + for ((i, argType) in argTypes.withIndex()) { + insns.add(VarInsnNode(ALOAD, 1)) + insns.add(pushInt(i)) + insns.add(InsnNode(AALOAD)) + emitUnbox(insns, argType) + insns.add(VarInsnNode(argType.getOpcode(ISTORE), nextSlot)) + nextSlot += argType.size + } + + // 复制原方法指令流(slot +2、return 装箱替换、label 映射) + val labelMap = HashMap() + for (insn in original.instructions) { + if (insn is LabelNode) labelMap[insn] = LabelNode() + } + + cloneInstructionsInto(insns, original, returnType, labelMap, ownerInternal, privateFields) + + // try-catch 表映射 + for (tcb in original.tryCatchBlocks) { + val start = labelMap[tcb.start] ?: continue + val end = labelMap[tcb.end] ?: continue + val handler = labelMap[tcb.handler] ?: continue + body.tryCatchBlocks.add(TryCatchBlockNode(start, end, handler, tcb.type)) + } + + // COMPUTE_MAXS / COMPUTE_FRAMES 会重算 + body.maxStack = 0 + body.maxLocals = 0 + + return body + } + + private fun hasUnsupportedInvokeSpecial(original: MethodNode, ownerInternal: String): Boolean { + for (insn in original.instructions) { + if (insn is MethodInsnNode && insn.opcode == INVOKESPECIAL) { + if (insn.owner == ownerInternal && insn.name != "") { + return true + } + } + } + return false + } + + /** JvmtiBackend 在 JNI 注册后的内部类名(可能被 Shadow 重定位) */ + private const val JVMTI_BACKEND = "taboolib/module/incision/loader/JvmtiBackend" + + /** + * 克隆原方法指令流到 [out],做 slot 偏移、return 替换、private 字段访问替换。 + * + * 规则: + * - [VarInsnNode].var 与 [IincInsnNode].var:+2 + * - xRETURN(基本类型):装箱 + ARETURN + * - RETURN(void):ACONST_NULL + ARETURN + * - ARETURN:保持 + * - GETFIELD/PUTFIELD 访问 private 字段:替换为 JNI 层 nFieldGet/nFieldSet + */ + private fun cloneInstructionsInto( + out: InsnList, + original: MethodNode, + returnType: Type, + labelMap: HashMap, + ownerInternal: String, + privateFields: Map, + ) { + for (insn in original.instructions) { + val cloned: AbstractInsnNode = insn.clone(labelMap) + + when (cloned) { + is VarInsnNode -> cloned.`var` += 2 + is IincInsnNode -> cloned.`var` += 2 + } + + // private 字段访问 → 通过 C 层 JNI 绕过访问控制 + if (cloned is FieldInsnNode && cloned.owner == ownerInternal && cloned.name in privateFields) { + when (cloned.opcode) { + GETFIELD -> { + // 栈顶: objectref → 需要: objectref, ownerClass, fieldName, fieldDesc → Object + // 获取 owner 的 Class 对象 + out.add(LdcInsnNode(Type.getObjectType(ownerInternal))) + out.add(LdcInsnNode(cloned.name)) + out.add(LdcInsnNode(cloned.desc)) + out.add(MethodInsnNode( + INVOKESTATIC, JVMTI_BACKEND, "nFieldGet", + "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + false + )) + // 返回的是 Object(已装箱),需要拆箱回原始类型 + val fieldType = Type.getType(cloned.desc) + emitUnbox(out, fieldType) + continue + } + PUTFIELD -> { + // 栈顶: objectref, value → 需要: objectref, ownerClass, fieldName, fieldDesc, value(boxed) + // 先装箱 value + val fieldType = Type.getType(cloned.desc) + emitBox(out, fieldType) + // 栈: objectref, boxedValue + // 需要重新排列为: objectref, ownerClass, fieldName, fieldDesc, boxedValue + // 用临时变量暂存 boxedValue + out.add(VarInsnNode(ASTORE, 0)) // 暂存到 slot 0(self 参数,此时已不需要) + // 栈: objectref + out.add(LdcInsnNode(Type.getObjectType(ownerInternal))) + out.add(LdcInsnNode(cloned.name)) + out.add(LdcInsnNode(cloned.desc)) + out.add(VarInsnNode(ALOAD, 0)) // 取回 boxedValue + out.add(MethodInsnNode( + INVOKESTATIC, JVMTI_BACKEND, "nFieldSet", + "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + false + )) + continue + } + GETSTATIC -> { + // 无栈顶对象,直接调用 nStaticFieldGet + out.add(LdcInsnNode(Type.getObjectType(ownerInternal))) + out.add(LdcInsnNode(cloned.name)) + out.add(LdcInsnNode(cloned.desc)) + out.add(MethodInsnNode( + INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldGet", + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + false + )) + val fieldType = Type.getType(cloned.desc) + emitUnbox(out, fieldType) + continue + } + PUTSTATIC -> { + // 栈顶: value → 需要: ownerClass, fieldName, fieldDesc, value(boxed) + val fieldType = Type.getType(cloned.desc) + emitBox(out, fieldType) + out.add(VarInsnNode(ASTORE, 0)) + out.add(LdcInsnNode(Type.getObjectType(ownerInternal))) + out.add(LdcInsnNode(cloned.name)) + out.add(LdcInsnNode(cloned.desc)) + out.add(VarInsnNode(ALOAD, 0)) + out.add(MethodInsnNode( + INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldSet", + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + false + )) + continue + } + } + } + + if (cloned is InsnNode) { + when (cloned.opcode) { + IRETURN, LRETURN, FRETURN, DRETURN -> { + emitBox(out, returnType) + out.add(InsnNode(ARETURN)) + continue + } + RETURN -> { + out.add(InsnNode(ACONST_NULL)) + out.add(InsnNode(ARETURN)) + continue + } + ARETURN -> { + out.add(cloned) + continue + } + } + } + + out.add(cloned) + } + } + + // ───────────────────────────────────────────────────────────────── + // 装箱 / 拆箱 / int 常量 + + /** + * 栈顶是 Object(args[i]),按 [target] 类型拆箱为对应原始值或引用。 + */ + private fun emitUnbox(out: InsnList, target: Type) { + when (target.sort) { + Type.BOOLEAN -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Boolean")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false)) + } + Type.BYTE -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Byte")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false)) + } + Type.CHAR -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Character")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false)) + } + Type.SHORT -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Short")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false)) + } + Type.INT -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Integer")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false)) + } + Type.LONG -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Long")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false)) + } + Type.FLOAT -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Float")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false)) + } + Type.DOUBLE -> { + out.add(TypeInsnNode(CHECKCAST, "java/lang/Double")) + out.add(MethodInsnNode(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false)) + } + Type.OBJECT, Type.ARRAY -> { + out.add(TypeInsnNode(CHECKCAST, target.internalName)) + } + else -> error("unsupported type sort: ${target.sort}") + } + } + + /** + * 栈顶是原始类型/引用值,装箱为 Object。 + */ + private fun emitBox(out: InsnList, source: Type) { + when (source.sort) { + Type.BOOLEAN -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false)) + Type.BYTE -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false)) + Type.CHAR -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false)) + Type.SHORT -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false)) + Type.INT -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false)) + Type.LONG -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false)) + Type.FLOAT -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false)) + Type.DOUBLE -> out.add(MethodInsnNode(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false)) + Type.OBJECT, Type.ARRAY -> { /* 已是引用,无需装箱 */ } + Type.VOID -> error("emitBox called with VOID") + else -> error("unsupported type sort: ${source.sort}") + } + } + + /** + * 产出把 int 常量压栈的最短指令。 + */ + private fun pushInt(value: Int): AbstractInsnNode { + return when { + value == -1 -> InsnNode(ICONST_M1) + value == 0 -> InsnNode(ICONST_0) + value == 1 -> InsnNode(ICONST_1) + value == 2 -> InsnNode(ICONST_2) + value == 3 -> InsnNode(ICONST_3) + value == 4 -> InsnNode(ICONST_4) + value == 5 -> InsnNode(ICONST_5) + value in Byte.MIN_VALUE..Byte.MAX_VALUE -> IntInsnNode(BIPUSH, value) + value in Short.MIN_VALUE..Short.MAX_VALUE -> IntInsnNode(SIPUSH, value) + else -> LdcInsnNode(value) + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BridgeClassLoader.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BridgeClassLoader.kt new file mode 100644 index 000000000..241740270 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BridgeClassLoader.kt @@ -0,0 +1,58 @@ +package taboolib.module.incision.weaver + +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList + +class BridgeClassLoader private constructor() : ClassLoader(null) { + + companion object { + + val INSTANCE = BridgeClassLoader() + + const val BODIES_SUFFIX = "$\$IncisionBodies" + + fun bodiesClassName(ownerInternalName: String): String { + return ownerInternalName.replace('/', '.') + BODIES_SUFFIX + } + } + + private val definedClasses = ConcurrentHashMap>() + private val delegates = CopyOnWriteArrayList>() + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + definedClasses[name]?.let { return it } + for (ref in delegates) { + val cl = ref.get() ?: continue + try { + return cl.loadClass(name) + } catch (_: ClassNotFoundException) { + } + } + return super.loadClass(name, resolve) + } + + fun defineBodies(binaryName: String, bytes: ByteArray): Class<*> { + definedClasses[binaryName]?.let { return it } + val cls = defineClass(binaryName, bytes, 0, bytes.size) + definedClasses[binaryName] = cls + return cls + } + + fun registerDelegate(cl: ClassLoader) { + for (ref in delegates) { + if (ref.get() === cl) return + } + delegates.add(WeakReference(cl)) + } + + fun unregisterDelegate(cl: ClassLoader) { + delegates.removeIf { it.get() === cl || it.get() == null } + } + + fun hasBodies(binaryName: String): Boolean = definedClasses.containsKey(binaryName) + + fun cleanup() { + delegates.removeIf { it.get() == null } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/FrameVerifier.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/FrameVerifier.kt new file mode 100644 index 000000000..c9bb90a24 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/FrameVerifier.kt @@ -0,0 +1,72 @@ +package taboolib.module.incision.weaver + +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.analysis.Analyzer +import org.objectweb.asm.tree.analysis.AnalyzerException +import org.objectweb.asm.tree.analysis.BasicValue +import org.objectweb.asm.tree.analysis.BasicVerifier + +/** + * Tree-API 织入后帧一致性预检。 + * + * 在 [ClassNode.accept] 到 [org.objectweb.asm.ClassWriter] 之前跑一遍: + * 若任何方法抛 [AnalyzerException],说明插入的 InsnList 破坏了栈 / 局部变量一致性, + * 此时应回退原字节码而不是把坏 class 喂给 JVM ——否则得到的是 runtime VerifyError。 + * + * 使用 [BasicVerifier] 而非简易 [org.objectweb.asm.tree.analysis.BasicInterpreter]: + * BasicVerifier 额外校验 INVOKE 参数类型、GETFIELD owner 类型等,更贴近 JVM 字节码校验器。 + * + * 返回的 [Report] 只保留发生问题的方法及其首错(同方法多错不重复收集,降低噪音)。 + */ +object FrameVerifier { + + data class Report( + val ok: Boolean, + val failures: List, + ) { + fun summary(): String = if (ok) "ok" else failures.joinToString("; ") { it.shortDesc() } + } + + data class Failure( + val method: String, + val descriptor: String, + val message: String, + val cause: Throwable, + ) { + fun shortDesc(): String = "$method$descriptor → $message" + } + + /** + * 跑 Analyzer over 所有方法。抽象 / 原生方法跳过(没有方法体)。 + * + * @param owner 类 internal name(用于 BasicVerifier.newOperation 识别 this 类型) + */ + fun verify(node: ClassNode): Report { + val owner = node.name + val failures = mutableListOf() + for (m in node.methods) { + if ((m.access and (org.objectweb.asm.Opcodes.ACC_ABSTRACT or org.objectweb.asm.Opcodes.ACC_NATIVE)) != 0) continue + val f = verifyMethod(owner, m) + if (f != null) failures += f + } + return Report(failures.isEmpty(), failures) + } + + /** + * 单方法校验。[BasicVerifier] 需要类层级 Resolver 才能正确判 CHECKCAST / INVOKE + * owner 继承关系;此处使用默认实现即可——默认假设任何两个引用兼容到 Object, + * 对"不破坏栈结构"这个目标足够。 + */ + private fun verifyMethod(owner: String, m: MethodNode): Failure? { + return try { + val analyzer = Analyzer(BasicVerifier()) + analyzer.analyze(owner, m) + null + } catch (e: AnalyzerException) { + Failure(m.name, m.desc, "${e.javaClass.simpleName}: ${e.message}", e) + } catch (t: Throwable) { + Failure(m.name, m.desc, "${t.javaClass.simpleName}: ${t.message}", t) + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt new file mode 100644 index 000000000..d72cfcd15 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt @@ -0,0 +1,520 @@ +package taboolib.module.incision.weaver + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.commons.ClassRemapper +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.cache.IncisionCache +import taboolib.module.incision.diagnostic.Forensics +import taboolib.module.incision.diagnostic.Trauma +import taboolib.module.incision.loader.JvmtiBackend +import taboolib.module.incision.remap.NameResolver +import taboolib.module.incision.remap.RemapRouter +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.weaver.site.SiteSpec +import java.io.File + +/** + * 主 weaver — 接收一个目标类的字节码,针对若干 target 方法注入 dispatcher 调用。 + * + * 注入的 JVM 调用签名(固定由 IncisionGate / TheatreDispatcher 持有): + * ``` + * INVOKESTATIC taboolib/module/incision/runtime/TheatreDispatcher.dispatch + * (Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; + * ``` + * 后续 GateBootstrapper 启用后会把调用重定向到 `taboolib/incision/gate/IncisionGate`。 + * + * 当前支持 Lead / Trail / Splice / Excise;Graft / Bypass / Trim 标记为待实现。 + */ +class Scalpel( + private val resolver: NameResolver = RemapRouter, + private val targetsByOwner: Map>, +) { + + data class AdviceTargetSpec( + val target: MethodCoordinate, + val kinds: Set, + val sites: List = emptyList(), + ) + + fun weave(originalBytes: ByteArray): ByteArray { + return try { + val probeReader = ClassReader(originalBytes) + val probeOwner = probeReader.className + val sourceBytes = JvmtiBackend.getCachedOriginal(probeOwner) ?: run { + JvmtiBackend.cacheOriginal(probeOwner, originalBytes) + originalBytes + } + val reader = ClassReader(sourceBytes) + val owner = reader.className + val targets = targetsByOwner[owner] ?: return originalBytes + val loader = currentTransformLoader.get() + val writer = SafeClassWriter(reader, loader) + val visitor = WeavingClassVisitor(Opcodes.ASM9, writer, targets) + val pipeline = if (resolver.isRemappableTarget(owner)) { + ClassRemapper(visitor, RemapperBridge(resolver)) + } else visitor + reader.accept(pipeline, ClassReader.EXPAND_FRAMES) + var bytes = writer.toByteArray() + // 二次 pass — 走 SiteWeaver 处理 GRAFT / BYPASS / TRIM 站点 + val anySites = targets.any { it.sites.isNotEmpty() } + if (anySites) { + bytes = applySiteWeaver(bytes, targets, loader) + } + generateAndRegisterBodies(owner, sourceBytes, targets) + devDumpIfEnabled(owner, sourceBytes, bytes) + bytes + } catch (t: Throwable) { + throw Trauma.Weaving.AsmVerifyError( + "", MethodCoordinate("?", "?", "?"), + verifyOutput = t.stackTraceToString(), cause = t + ) + } + } + + companion object { + /** + * 由 [InstrumentationBackend] 在 transform 回调中设置,携带目标类的 ClassLoader。 + * [SafeClassWriter.getClassLoader] 读取此值,让 getCommonSuperClass 的 Class.forName + * 能看见 fixture / 插件 ClassLoader 上的类型,避免帧合并回退 Object 引发 VerifyError。 + */ + val currentTransformLoader: ThreadLocal = ThreadLocal.withInitial { null } + } + + private fun configDigest(targets: List): String = buildString { + for (t in targets) { + append(t.target.owner).append('#').append(t.target.name).append(t.target.descriptor) + append('|').append(t.kinds.joinToString(",") { it.name }) + for (s in t.sites) { + append("|site:").append(s.anchor).append(':').append(s.kind).append(':') + .append(s.ownerPattern).append('.').append(s.namePattern).append(s.descPattern) + .append('@').append(s.shift).append('#').append(s.ordinal) + } + append(';') + } + } + + private fun devDumpIfEnabled(owner: String, before: ByteArray, after: ByteArray) { + if (System.getProperty("incision.dev", "false").toBoolean().not()) return + val dir = File(System.getProperty("incision.devDir", ".dev/incision")) + IncisionCache.dump(dir, owner.replace('/', '.'), before, after) + } + + /** + * 第二 pass — 站点织入。采用 Tree API 流水: + * + * 1. `ClassReader.accept(ClassNode, EXPAND_FRAMES)` 读入完整树 + * 2. 对每个需要织入的 method,用 [SiteWeaver.wrapMethod] 包装成 [TreeMethodDriver], + * 然后 `method.accept(driver)` 重放原方法进 driver;driver.visitEnd 完成 instructions 变异并发射到新 MethodNode + * 3. 变异后的方法替换 ClassNode.methods 里对应项 + * 4. [FrameVerifier] 用 `Analyzer` 跑一遍做帧一致性预检;若失败,回退原字节码 + * 5. `ClassNode.accept(ClassWriter(COMPUTE_FRAMES))` 写出 + * + * 回退策略:Forensics.warn 记录失败方法与原因,返回 `bytes` 不动。保守避免把坏 class 喂给 JVM 触发 VerifyError。 + */ + private fun applySiteWeaver(bytes: ByteArray, targets: List, loader: ClassLoader?): ByteArray { + val reader = ClassReader(bytes) + val original = org.objectweb.asm.tree.ClassNode() + reader.accept(original, ClassReader.EXPAND_FRAMES) + + val weaved = org.objectweb.asm.tree.ClassNode() + // 拷贝类级元数据到新 ClassNode;方法清单按需替换 + weaved.visit( + original.version, original.access, original.name, original.signature, + original.superName, original.interfaces.toTypedArray() + ) + weaved.sourceFile = original.sourceFile + weaved.sourceDebug = original.sourceDebug + weaved.outerClass = original.outerClass + weaved.outerMethod = original.outerMethod + weaved.outerMethodDesc = original.outerMethodDesc + weaved.innerClasses = original.innerClasses + weaved.nestHostClass = original.nestHostClass + weaved.nestMembers = original.nestMembers + weaved.permittedSubclasses = original.permittedSubclasses + weaved.visibleAnnotations = original.visibleAnnotations + weaved.invisibleAnnotations = original.invisibleAnnotations + weaved.visibleTypeAnnotations = original.visibleTypeAnnotations + weaved.invisibleTypeAnnotations = original.invisibleTypeAnnotations + weaved.attrs = original.attrs + weaved.fields = original.fields + + for (method in original.methods) { + val sites = targets + .filter { it.target.name == method.name && matchesDesc(it.target.descriptor, method.desc) } + .flatMap { it.sites } + if (sites.isNullOrEmpty()) { + weaved.methods.add(method) + continue + } + // 把原方法 replay 给新的 MethodNode(driver 是 MethodNode),visitEnd 触发 Tree 变异 + val rewritten = org.objectweb.asm.tree.MethodNode( + Opcodes.ASM9, method.access, method.name, method.desc, method.signature, + method.exceptions?.toTypedArray() + ) + val driver = SiteWeaver(sites).wrapMethod( + Opcodes.ASM9, rewritten, + method.access, method.name, method.desc, method.signature, + method.exceptions?.toTypedArray() + ) + method.accept(driver) + weaved.methods.add(rewritten) + } + + // 先让 ClassWriter 以 COMPUTE_MAXS | COMPUTE_FRAMES 产出字节, + // 再用 ClassReader 重读出来——此时 maxStack / maxLocals / frames 全部已由 ASM 重算, + // FrameVerifier 校验的是最终产物的真实状态,而不是 MethodNode 残留的旧 maxStack。 + // 先跑一遍 FrameVerifier(Analyzer)做方法级预检,把 ClassWriter COMPUTE_FRAMES + // 一旦挂会整个 class 回退的行为,降维成"哪个方法坏了哪个方法回退"。这里只拿诊断信息,不阻塞写出。 + val preReport = FrameVerifier.verify(weaved) + if (!preReport.ok) { + preReport.failures.forEach { f -> + Forensics.error( + "SiteWeaver 写出前 FrameVerifier 预检失败 — ${original.name}.${f.method}${f.descriptor} — ${f.message}", + f.cause, + ) + } + } + + val writer = SafeClassWriter(reader, loader) + val outBytes = try { + weaved.accept(writer) + writer.toByteArray() + } catch (t: Throwable) { + Forensics.error( + "SiteWeaver ClassWriter 写出失败 — owner=${original.name} — ${t.javaClass.name}: ${t.message};尝试逐方法定位", + t, + ) + // 逐方法写,定位到具体哪个方法触发 Frame.merge AIOOB + for (m in weaved.methods) { + val probeNode = org.objectweb.asm.tree.ClassNode().also { + it.version = weaved.version + it.access = weaved.access + it.name = weaved.name + it.superName = weaved.superName + it.interfaces = weaved.interfaces + } + probeNode.methods.add(m) + try { + probeNode.accept(SafeClassWriter(reader, loader)) + } catch (mt: Throwable) { + Forensics.error( + "坏方法定位:${original.name}.${m.name}${m.desc} — ${mt.javaClass.name}: ${mt.message}", + mt, + ) + } + probeNode.methods.clear() + } + return bytes + } + + val verified = org.objectweb.asm.tree.ClassNode() + org.objectweb.asm.ClassReader(outBytes).accept(verified, 0) + val report = FrameVerifier.verify(verified) + if (!report.ok) { + Forensics.warn( + "SiteWeaver FrameVerifier 失败 — owner=${original.name} ${report.summary()};回退原字节码" + ) + report.failures.forEach { f -> + Forensics.error("FrameVerifier failure: ${original.name}.${f.method}${f.descriptor} — ${f.message}", f.cause) + } + return bytes + } + + return outBytes + } + + private val bodyKinds = setOf(AdviceKind.SPLICE, AdviceKind.EXCISE, AdviceKind.BYPASS) + + private fun generateAndRegisterBodies( + owner: String, + originalBytes: ByteArray, + targets: List, + ) { + val spliceMethods: Set> = targets + .filter { it.kinds.any { k -> k in bodyKinds } } + .map { it.target.name to it.target.descriptor } + .toSet() + if (spliceMethods.isEmpty()) return + val bodiesName = BridgeClassLoader.bodiesClassName(owner) + if (BridgeClassLoader.INSTANCE.hasBodies(bodiesName)) return + val sourceBytes = try { + JvmtiBackend.getCachedOriginal(owner) + } catch (_: Throwable) { null } ?: run { + try { + JvmtiBackend.cacheOriginal(owner, originalBytes) + } catch (t: Throwable) { + Forensics.error("JvmtiBackend.cacheOriginal failed: $owner — ${t.message}", t) + } + originalBytes + } + val bodiesBytes = try { + BodiesClassGenerator.generate(sourceBytes, owner, spliceMethods) + } catch (t: Throwable) { + Forensics.error("BodiesClassGenerator.generate failed: $owner — ${t.message}", t) + null + } + if (bodiesBytes == null) return + try { + BridgeClassLoader.INSTANCE.defineBodies(bodiesName, bodiesBytes) + // 注册能解析目标类的 ClassLoader 为 delegate + // contextClassLoader 不一定是插件 CL,所以多注册几个来源 + Thread.currentThread().contextClassLoader?.let { + BridgeClassLoader.INSTANCE.registerDelegate(it) + } + Scalpel::class.java.classLoader?.let { + BridgeClassLoader.INSTANCE.registerDelegate(it) + } + Forensics.debug("Bodies class defined: $bodiesName") + } catch (t: Throwable) { + Forensics.error("Bodies class define failed: $bodiesName — ${t.message}", t) + } + } + + private fun matchesDesc(pattern: String, actual: String): Boolean { + if (pattern == actual) return true + if (pattern == "(*)" || pattern == "(*)V" || pattern == "()*") return true + return false + } + + private class RemapperBridge(val r: NameResolver) : org.objectweb.asm.commons.Remapper() { + override fun map(internalName: String): String = r.resolveOwner(internalName) + override fun mapMethodName(owner: String, name: String, descriptor: String): String = + r.resolveMethod(owner, name, descriptor).first + override fun mapFieldName(owner: String, name: String, descriptor: String): String = + r.resolveField(owner, name, descriptor).first + } + + private class WeavingClassVisitor( + api: Int, cv: ClassWriter, + val targets: List, + ) : org.objectweb.asm.ClassVisitor(api, cv) { + + private lateinit var className: String + override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array?) { + className = name + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor { + val base = super.visitMethod(access, name, descriptor, signature, exceptions) + val matching = targets.filter { matchesDescriptor(it.target.descriptor, descriptor) && it.target.name == name } + if (matching.isEmpty()) return base + val mergedKinds = mutableSetOf() + for (s in matching) mergedKinds.addAll(s.kinds) + val mergedSpec = AdviceTargetSpec(matching.first().target, mergedKinds, matching.flatMap { it.sites }) + Forensics.debug("weave inject $className.$name$descriptor kinds=$mergedKinds static=${(access and Opcodes.ACC_STATIC) != 0}") + return AdviceInjector(api, base, access, name, descriptor, className, mergedSpec) + } + + private fun matchesDescriptor(pattern: String, actual: String): Boolean { + if (pattern == actual) return true + if (pattern == "(*)" || pattern == "(*)V" || pattern == "()*") return true + // 通配实参或返回值 + val pOpen = pattern.indexOf('('); val pClose = pattern.indexOf(')') + val aOpen = actual.indexOf('('); val aClose = actual.indexOf(')') + if (pOpen < 0 || pClose < 0 || aOpen < 0 || aClose < 0) return false + val pArgs = pattern.substring(pOpen + 1, pClose) + val pRet = pattern.substring(pClose + 1) + val aArgs = actual.substring(aOpen + 1, aClose) + val aRet = actual.substring(aClose + 1) + val argsOk = pArgs == "*" || pArgs == aArgs + val retOk = pRet == "*" || pRet == aRet + return argsOk && retOk + } + + } + + /** + * 为单个方法注入 advice 调用。 + * + * - **Lead**: onMethodEnter 处调用 dispatcher,丢弃返回值 + * - **Trail**: onMethodExit(非异常)处调用 dispatcher,丢弃返回值 + * - **Splice**: onMethodEnter 处调用 dispatcher;若返回值非 null 则按方法返回类型转换并直接 return + * - **Excise**: 整段替换 — 在 onMethodEnter 处调用 dispatcher 并直接 return(忽略原方法体的剩余指令;ASM AdviceAdapter 不能"删除剩余指令",因此采用:dispatcher 返回 null 时回退原方法) + * - **Graft / Bypass / Trim**: 需要 @Site 锚点扫描,本 weaver 暂不实现,运行时由 dispatcher 链兜底(仍触发 handler,但不能改写 INVOKE / FIELD_GET 等指令位置) + */ + private class AdviceInjector( + api: Int, mv: MethodVisitor, access: Int, + val methodName: String, val methodDesc: String, + val ownerName: String, + val spec: AdviceTargetSpec, + ) : AdviceAdapter(api, mv, access, methodName, methodDesc) { + + // 使用用户声明的 target.signature 作为 dispatch key, + // 保持与 TheatreDispatcher.chains 的 key 一致(remap 前的名字) + private val targetSig = spec.target.signature + private val returnType: org.objectweb.asm.Type = org.objectweb.asm.Type.getReturnType(methodDesc) + private val kindsWithSite = spec.sites.map { it.kind }.toSet() + private val entryKinds = spec.kinds - kindsWithSite + + override fun onMethodEnter() { + if (AdviceKind.LEAD in entryKinds) { + emitDispatcherCall("@LEAD") + pop() + } + // 入口 @SPLICE 只对"无 site 的 SPLICE/EXCISE/BYPASS/GRAFT/TRIM"开。GRAFT/BYPASS/TRIM + // 一旦带 site,就由 SiteWeaver 在锚点位置发射 dispatcher——再在入口插一发会让 handler 触发 + // 2 次(曾观察到 surgeon-graft-field-get expected=1 actual=2、surgeon-bypass-invoke + // expected=2000 actual=1000 等),因为 TheatreDispatcher.matchesPhase 把 + // SPLICE/EXCISE/BYPASS/GRAFT/TRIM 都映到 @SPLICE 相位。 + val hasSplicePhase = entryKinds.any { + it == AdviceKind.SPLICE || it == AdviceKind.EXCISE + || it == AdviceKind.BYPASS || it == AdviceKind.GRAFT || it == AdviceKind.TRIM + } + if (hasSplicePhase) { + emitDispatcherCall("@SPLICE") + emitReturnIfNonNull() + } + } + + override fun onMethodExit(opcode: Int) { + if (AdviceKind.TRAIL !in entryKinds) return + if (opcode == ATHROW) { + emitThrowTrailCall() + } else { + emitDispatcherCall("@TRAIL") + pop() + } + } + + /** + * 异常出口(ATHROW)的 TRAIL 调度: + * 栈前 [thr] → 栈后 [thr] + * 把 throwable 作为 args[0] 传入 dispatch,phase=@TRAIL_THROW; + * dispatcher 侧按 entry.onThrow 过滤并回填 Theatre.throwable。 + */ + private fun emitThrowTrailCall() { + val thrLocal = newLocal(org.objectweb.asm.Type.getType("Ljava/lang/Throwable;")) + visitVarInsn(Opcodes.ASTORE, thrLocal) + visitLdcInsn("$targetSig@TRAIL_THROW") + if ((methodAccess and Opcodes.ACC_STATIC) != 0) { + visitInsn(Opcodes.ACONST_NULL) + } else { + loadThis() + } + visitInsn(Opcodes.ICONST_1) + visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object") + visitInsn(Opcodes.DUP) + visitInsn(Opcodes.ICONST_0) + visitVarInsn(Opcodes.ALOAD, thrLocal) + visitInsn(Opcodes.AASTORE) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/izzel/incision/bridge/IncisionBridge", + "dispatch", + "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", + false, + ) + visitInsn(Opcodes.POP) + visitVarInsn(Opcodes.ALOAD, thrLocal) + } + + /** + * stack: (Object) — 若 != null,按 returnType 拆箱并 return;否则 pop 继续原方法体 + */ + private fun emitReturnIfNonNull() { + val skipLabel = newLabel() + // dup; ifnull skip + visitInsn(Opcodes.DUP) + visitJumpInsn(Opcodes.IFNULL, skipLabel) + // 非 null,按返回类型拆箱并 return + when (returnType.sort) { + org.objectweb.asm.Type.VOID -> { + pop() + visitInsn(Opcodes.RETURN) + } + org.objectweb.asm.Type.BOOLEAN -> { + unboxAndReturn("java/lang/Boolean", "booleanValue", "()Z", Opcodes.IRETURN) + } + org.objectweb.asm.Type.BYTE -> { + unboxAndReturn("java/lang/Byte", "byteValue", "()B", Opcodes.IRETURN) + } + org.objectweb.asm.Type.CHAR -> { + unboxAndReturn("java/lang/Character", "charValue", "()C", Opcodes.IRETURN) + } + org.objectweb.asm.Type.SHORT -> { + unboxAndReturn("java/lang/Short", "shortValue", "()S", Opcodes.IRETURN) + } + org.objectweb.asm.Type.INT -> { + unboxAndReturn("java/lang/Integer", "intValue", "()I", Opcodes.IRETURN) + } + org.objectweb.asm.Type.LONG -> { + unboxAndReturn("java/lang/Long", "longValue", "()J", Opcodes.LRETURN) + } + org.objectweb.asm.Type.FLOAT -> { + unboxAndReturn("java/lang/Float", "floatValue", "()F", Opcodes.FRETURN) + } + org.objectweb.asm.Type.DOUBLE -> { + unboxAndReturn("java/lang/Double", "doubleValue", "()D", Opcodes.DRETURN) + } + else -> { + visitTypeInsn(Opcodes.CHECKCAST, returnType.internalName) + visitInsn(Opcodes.ARETURN) + } + } + visitLabel(skipLabel) + // 走 null 分支:栈顶仍有 null,pop 掉 + visitInsn(Opcodes.POP) + } + + private fun unboxAndReturn(boxedInternal: String, method: String, desc: String, retOp: Int) { + visitTypeInsn(Opcodes.CHECKCAST, boxedInternal) + visitMethodInsn(Opcodes.INVOKEVIRTUAL, boxedInternal, method, desc, false) + visitInsn(retOp) + } + + private fun emitDispatcherCall(phaseSuffix: String = "") { + visitLdcInsn("$targetSig$phaseSuffix") + if ((methodAccess and Opcodes.ACC_STATIC) != 0) { + visitInsn(Opcodes.ACONST_NULL) + } else { + loadThis() + } + loadArgArray() + // !! 必须使用非 taboolib.* 的包名,否则会被 taboolib-gradle-plugin + // 的 RelocateRemapper 在每个插件打包时重定向为 .taboolib.*, + // 导致跨插件无法共享同一桥。 + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/izzel/incision/bridge/IncisionBridge", + "dispatch", + "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", + false, + ) + } + } + + /** + * JVMTI 安全的 ClassWriter — 重写 getCommonSuperClass / getClassLoader 以避免跨 ClassLoader 加载失败。 + * + * 默认实现使用 Class.forName() 并以 ClassWriter 自己的 ClassLoader 为起点;在 JVMTI retransform + * 场景下目标类的依赖(如插件 fixture)可能不在 ASM 所在 ClassLoader 中,导致 TypeNotPresentException + * → 帧计算失败 → JVM 静默拒绝 retransformed 字节码(甚至把 this slot 降格为 Object 造成 VerifyError)。 + * + * 修复: + * 1. type1==type2 短路,Object 参与合并直接返回 Object —— 覆盖 50%+ 常见场景无需 Class.forName。 + * 2. [getClassLoader] 优先返回 [targetLoader](由 transformer 传入,等于目标类的定义 CL), + * Class.forName 即可看见 fixture / 插件类型,彻底消除 frame 回退 Object 的诱因。 + * 3. 任何失败兜底 Object,避免 weave 整体崩盘。 + */ + private class SafeClassWriter(reader: ClassReader, private val targetLoader: ClassLoader?) : + ClassWriter(reader, COMPUTE_MAXS or COMPUTE_FRAMES) { + + override fun getClassLoader(): ClassLoader = + targetLoader ?: Thread.currentThread().contextClassLoader ?: SafeClassWriter::class.java.classLoader + + override fun getCommonSuperClass(type1: String, type2: String): String { + if (type1 == type2) return type1 + if (type1 == "java/lang/Object" || type2 == "java/lang/Object") return "java/lang/Object" + return try { + super.getCommonSuperClass(type1, type2) + } catch (_: Throwable) { + "java/lang/Object" + } + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/SiteWeaver.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/SiteWeaver.kt new file mode 100644 index 000000000..a9c588bb8 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/SiteWeaver.kt @@ -0,0 +1,984 @@ +package taboolib.module.incision.weaver + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.IincInsnNode +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.InvokeDynamicInsnNode +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.LabelNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.LookupSwitchInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.MultiANewArrayInsnNode +import org.objectweb.asm.tree.TableSwitchInsnNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode +import taboolib.module.incision.annotation.Trim +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.Shift +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.emitter.DispatcherEmitter +import taboolib.module.incision.weaver.site.matcher.FieldMatcher +import taboolib.module.incision.weaver.site.matcher.InvokeMatcher +import taboolib.module.incision.weaver.site.matcher.MatchEvent +import taboolib.module.incision.weaver.site.matcher.NewMatcher +import taboolib.module.incision.weaver.site.matcher.OpcodeSeqMatcher +import taboolib.module.incision.weaver.site.matcher.TerminalMatcher +import taboolib.module.incision.weaver.site.pattern.SitePattern +import taboolib.module.incision.weaver.site.planner.EmissionPlan +import taboolib.module.incision.weaver.site.planner.PlannerDispatcher +import taboolib.module.incision.weaver.site.recorder.InsnAction +import taboolib.module.incision.weaver.site.recorder.RecordingMethodVisitor +import taboolib.module.incision.weaver.site.recorder.Replayer + +/** + * 站点织入器 —— 组合根。 + * + * 有两条执行分支,由 [SiteSpec.pattern] / [SiteSpec.offset] / anchor==HEAD 联合决定: + * - **streaming(默认)**:无 OpcodeSeq、无非零 offset、无 HEAD 锚点时,直接在 ASM visitor 流中 + * 发射 dispatcher 调用(见 [wrapMethodStreaming])。 + * - **recording(扩展)**:遇到以上任一条件时,先录制完整方法指令流,离线 scan → match → plan + * → insert → replay(见 [wrapMethodRecording])。 + * + * 对外 API 只有 [wrapMethod] 一条;调用方无需关心路径选择,由 sites 自派生。 + */ +class SiteWeaver(private val sites: List) { + + companion object MetadataResolver { + + /** + * TRIM RETURN 模式下用 anchor 节点反推栈顶值描述符。详见 [SiteSpec.trimReturnDescriptor]。 + * 对应 streaming 路径的等价入参版本:[resolveTrimReturnDescStreaming]。 + */ + fun resolveTrimReturnDesc(anchor: AbstractInsnNode, hostDesc: String, site: SiteSpec): SiteSpec { + if (site.kind != AdviceKind.TRIM) return site + if (site.trimKind != Trim.Kind.RETURN && site.trimKind != null) return site + if (site.trimReturnDescriptor.isNotEmpty()) return site + val derived: String = when (anchor) { + is MethodInsnNode -> returnPart(anchor.desc) + is FieldInsnNode -> if (anchor.opcode == Opcodes.GETFIELD || anchor.opcode == Opcodes.GETSTATIC) anchor.desc else "" + is InsnNode -> if (anchor.opcode in Opcodes.IRETURN..Opcodes.ARETURN) returnPart(hostDesc) else "" + else -> "" + } + return if (derived.isNotEmpty()) site.copy(trimReturnDescriptor = derived) else site + } + + /** streaming 路径的 TRIM RETURN 描述符推断(无 anchor 节点,按 anchor 类型 + 元数据)。 */ + fun resolveTrimReturnDescStreaming( + anchor: Anchor, opcode: Int, anchorDesc: String, hostDesc: String, site: SiteSpec, + ): SiteSpec { + if (site.kind != AdviceKind.TRIM) return site + if (site.trimKind != Trim.Kind.RETURN && site.trimKind != null) return site + if (site.trimReturnDescriptor.isNotEmpty()) return site + val derived: String = when (anchor) { + Anchor.INVOKE -> returnPart(anchorDesc) + Anchor.FIELD_GET -> anchorDesc + Anchor.RETURN -> returnPart(hostDesc) + else -> "" + } + return if (derived.isNotEmpty()) site.copy(trimReturnDescriptor = derived) else site + } + + /** + * TRIM ARG:把"逻辑参数序号"翻译成宿主方法的 LV 物理 slot(实例方法 +1,J/D 双宽累加)。 + * 详见 [SiteSpec.trimIndex] / [SiteSpec.trimArgDescriptor]。 + */ + fun resolveTrimSlot(hostAccess: Int, hostDesc: String, site: SiteSpec): SiteSpec { + if (site.kind != AdviceKind.TRIM || site.trimKind != Trim.Kind.ARG) return site + val argDescs = parseArgDescriptors(hostDesc) + val logical = site.trimIndex + if (logical < 0 || logical >= argDescs.size) return site + val isStatic = (hostAccess and Opcodes.ACC_STATIC) != 0 + var slot = if (isStatic) 0 else 1 + for (i in 0 until logical) slot += slotWidth(argDescs[i]) + val patched = if (site.trimArgDescriptor.isEmpty()) argDescs[logical] else site.trimArgDescriptor + return site.copy(trimIndex = slot, trimArgDescriptor = patched) + } + + /** Tree 路径:BYPASS 元数据(消费描述符 + 返回描述符 + tmp slot)。 */ + fun resolveBypassMeta(anchor: AbstractInsnNode, hostMaxLocals: Int, site: SiteSpec): SiteSpec { + if (site.kind != AdviceKind.BYPASS) return site + val (consumed, ret) = when (anchor) { + is MethodInsnNode -> { + val args = parseArgDescriptors(anchor.desc).joinToString("") + val receiver = if (anchor.opcode == Opcodes.INVOKESTATIC) "" else "L${anchor.owner};" + receiver + args to returnPart(anchor.desc) + } + is FieldInsnNode -> when (anchor.opcode) { + Opcodes.GETFIELD -> "L${anchor.owner};" to anchor.desc + Opcodes.GETSTATIC -> "" to anchor.desc + Opcodes.PUTFIELD -> "L${anchor.owner};${anchor.desc}" to "V" + Opcodes.PUTSTATIC -> anchor.desc to "V" + else -> "" to "" + } + else -> "" to "" + } + if (ret.isEmpty()) return site + return site.copy(bypassConsumedDescs = consumed, bypassReturnDescriptor = ret, bypassTempSlot = hostMaxLocals) + } + + /** Streaming 路径:BYPASS 元数据(按 anchor opcode + 元数据,tmp slot 由调用方提供)。 */ + fun resolveBypassMetaStreaming( + anchor: Anchor, opcode: Int, anchorOwner: String, anchorDesc: String, + tmpSlot: Int, site: SiteSpec, + ): SiteSpec { + if (site.kind != AdviceKind.BYPASS) return site + val (consumed, ret) = when (anchor) { + Anchor.INVOKE -> { + val args = parseArgDescriptors(anchorDesc).joinToString("") + val receiver = if (opcode == Opcodes.INVOKESTATIC) "" else "L$anchorOwner;" + receiver + args to returnPart(anchorDesc) + } + Anchor.FIELD_GET -> when (opcode) { + Opcodes.GETFIELD -> "L$anchorOwner;" to anchorDesc + Opcodes.GETSTATIC -> "" to anchorDesc + else -> "" to "" + } + Anchor.FIELD_PUT -> when (opcode) { + Opcodes.PUTFIELD -> "L$anchorOwner;$anchorDesc" to "V" + Opcodes.PUTSTATIC -> anchorDesc to "V" + else -> "" to "" + } + else -> "" to "" + } + if (ret.isEmpty()) return site + return site.copy(bypassConsumedDescs = consumed, bypassReturnDescriptor = ret, bypassTempSlot = tmpSlot) + } + + private fun returnPart(methodDesc: String): String { + val close = methodDesc.indexOf(')') + return if (close in 0 until methodDesc.length - 1) methodDesc.substring(close + 1) else "" + } + + fun parseArgDescriptors(methodDesc: String): List { + val open = methodDesc.indexOf('(') + val close = methodDesc.indexOf(')') + if (open < 0 || close <= open) return emptyList() + val args = methodDesc.substring(open + 1, close) + val out = mutableListOf() + var i = 0 + while (i < args.length) { + val start = i + while (i < args.length && args[i] == '[') i++ + when (args.getOrNull(i)) { + 'L' -> { + val semi = args.indexOf(';', i) + if (semi < 0) return out + out += args.substring(start, semi + 1); i = semi + 1 + } + null -> return out + else -> { out += args.substring(start, i + 1); i++ } + } + } + return out + } + + fun slotWidth(d: String): Int = if (d == "J" || d == "D") 2 else 1 + + /** 估算 streaming 路径下足以放 tmp 的安全 slot:宿主参数(含 this)总宽度。ClassWriter COMPUTE_MAXS 会再补齐。 */ + fun estimateBypassTmpSlot(hostAccess: Int, hostDesc: String): Int { + val base = if ((hostAccess and Opcodes.ACC_STATIC) != 0) 0 else 1 + return parseArgDescriptors(hostDesc).fold(base) { acc, d -> acc + slotWidth(d) } + } + + fun resolveDispatchContext(hostAccess: Int, hostDesc: String, site: SiteSpec): SiteSpec { + val hostIsStatic = (hostAccess and Opcodes.ACC_STATIC) != 0 + if (site.hostMethodDescriptor == hostDesc && site.hostIsStatic == hostIsStatic) return site + return site.copy(hostMethodDescriptor = hostDesc, hostIsStatic = hostIsStatic) + } + } + + private val hasOpcodeSeq: Boolean = sites.any { it.pattern is SitePattern.OpcodeSeq } + private val hasNonZeroOffset: Boolean = sites.any { it.offset != 0 } + private val hasHead: Boolean = sites.any { it.anchor == Anchor.HEAD } + + fun wrapMethod( + api: Int, + delegate: MethodVisitor, + access: Int = 0, + name: String = "", + descriptor: String = "", + signature: String? = null, + exceptions: Array? = null, + ): MethodVisitor { + if (sites.isEmpty()) return delegate + return if (shouldStream()) wrapMethodStreaming(api, delegate, access, descriptor) + else wrapMethodRecording(api, delegate, access, name, descriptor, signature, exceptions) + } + + private fun shouldStream(): Boolean = + PlannerDispatcher.canStream(sites, hasOpcodeSeq) && !hasNonZeroOffset && !hasHead + + private fun wrapMethodStreaming(api: Int, delegate: MethodVisitor, access: Int, descriptor: String): MethodVisitor = + SiteVisitor(api, delegate, sites, access, descriptor) + + /** + * recording 路径 — Tree API 形态:把方法完整收进 [MethodNode],在 visitEnd 时按 + * [EmissionPlan] 把 [InsnList] 原位 insert 到 [MethodNode.instructions];随后 `accept(delegate)` + * 回放到下游。节点引用天然稳定,插入不影响其他 plan 的锚点。 + * + * 相比旧的 record→InsnAction→Replayer 链路,本路径: + * 1. 不再显式重演 `visitFrame` / 自造 Label —— MethodNode 自身负责 tree ↔ visitor 双向转换 + * 2. 发射的 [InsnList] 由 [DispatcherEmitter.buildGraft] 等产出,Label 以 [org.objectweb.asm.tree.LabelNode] 形态 + * 进入 tree,ClassWriter 的 COMPUTE_FRAMES 能正常推导 + */ + private fun wrapMethodRecording( + api: Int, + delegate: MethodVisitor, + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array?, + ): MethodVisitor = + TreeMethodDriver(api, access, name, descriptor, signature, exceptions, delegate, sites) + + /** + * streaming 路径内核。 + * + * 关键转变:所有 BYPASS / TRIM / GRAFT 的字节码发射都委托给 [DispatcherEmitter.build*] + * 然后 `accept(mv)` 回放,与 [TreeMethodDriver] 完全共享一份发射流水线 —— 避免双维护 + * 导致 #34/#36/#37 这类 fix 只在 recording 路径生效、streaming 路径 silently 走老逻辑。 + * + * 元数据补齐通过 [MetadataResolver.resolve*Streaming] 完成,依赖宿主方法的 `access` / + * `descriptor`(this 偏移、参数物理 slot、tmp slot 估算)。 + */ + private class SiteVisitor( + api: Int, + mv: MethodVisitor, + rawSites: List, + private val hostAccess: Int, + private val hostDesc: String, + ) : MethodVisitor(api, mv) { + + // 防御性去重:同一 anchor+pattern+kind+target+shift+ordinal 完全一致的 SiteSpec 重复入队会让 + // ordinal 过滤失效(每条独立计数 → 多 hit)。Scalpel 累积路径可能产生重复,先收敛。 + // 注意:仅 `SiteSpec` 所有字段相等才视为同一条,不会误合并 kind/target 不同的 advice。 + private val sites: List = rawSites.distinct() + + // per-site 计数器 —— 每条 SiteSpec 独立推进 idx。不同 site 即使同 target 也互不影响, + // 避免"共享 idx"模型下 ordinal=-1 的 advice 把 idx 提前推掉导致 ordinal>=0 的兄弟丢匹配。 + private val counters = IntArray(sites.size) + + // 每个宿主方法预留一个 tmp slot 给 BYPASS 用(保存 dispatch 返回 Object)。COMPUTE_MAXS 会再补齐。 + private val bypassTmpSlot: Int = MetadataResolver.estimateBypassTmpSlot(hostAccess, hostDesc) + + override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) { + val matched = matchFor(Anchor.INVOKE, owner, name, descriptor) + if (matched.isEmpty()) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + return + } + val isVoid = descriptor.endsWith(")V") + val enriched = matched.map { enrich(it, Anchor.INVOKE, opcode, owner, descriptor) } + applySites(enriched, isVoid) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) } + } + + override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { + val anchor = when (opcode) { + Opcodes.GETFIELD, Opcodes.GETSTATIC -> Anchor.FIELD_GET + Opcodes.PUTFIELD, Opcodes.PUTSTATIC -> Anchor.FIELD_PUT + else -> null + } + if (anchor == null) { + super.visitFieldInsn(opcode, owner, name, descriptor) + return + } + val matched = matchFor(anchor, owner, name, descriptor) + if (matched.isEmpty()) { + super.visitFieldInsn(opcode, owner, name, descriptor) + return + } + val isVoid = anchor == Anchor.FIELD_PUT + val enriched = matched.map { enrich(it, anchor, opcode, owner, descriptor) } + applySites(enriched, isVoid) { super.visitFieldInsn(opcode, owner, name, descriptor) } + } + + override fun visitTypeInsn(opcode: Int, type: String) { + if (opcode != Opcodes.NEW) { + super.visitTypeInsn(opcode, type) + return + } + val matched = matchFor(Anchor.NEW, type, "", "") + if (matched.isEmpty()) { + super.visitTypeInsn(opcode, type) + return + } + val enriched = matched.map { enrich(it, Anchor.NEW, opcode, type, "") } + applySites(enriched, isVoid = false) { super.visitTypeInsn(opcode, type) } + } + + override fun visitInsn(opcode: Int) { + val anchor = when { + opcode == Opcodes.ATHROW -> Anchor.THROW + opcode in Opcodes.IRETURN..Opcodes.RETURN -> Anchor.RETURN + else -> null + } + if (anchor == null) { + super.visitInsn(opcode) + return + } + val matched = matchFor(anchor, "", "", "") + if (matched.isEmpty()) { + super.visitInsn(opcode) + return + } + val enriched = matched.map { enrich(it, anchor, opcode, "", "") } + // RETURN/THROW 是消费栈顶的终止指令——TRIM 必须在 emitOriginal 之前发射,否则 + // 落到 unreachable 代码区,Analyzer 视作空栈,DUP 立刻 "Cannot pop operand off an empty stack" + applySites(enriched, isVoid = true, terminalConsumer = true) { super.visitInsn(opcode) } + } + + /** + * per-site 计数:每条 site 独立推进自己的 idx;ordinal=-1 通配,否则精确匹配第 N 次命中。 + */ + private fun matchFor(anchor: Anchor, owner: String, name: String, descriptor: String): List { + var out: MutableList? = null + for (i in sites.indices) { + val site = sites[i] + if (site.anchor != anchor) continue + val ownerOk = site.ownerPattern.isEmpty() || site.ownerPattern == owner + val nameOk = site.namePattern.isEmpty() || site.namePattern == name + val descOk = site.descPattern.isEmpty() || site.descPattern == descriptor + if (!ownerOk || !nameOk || !descOk) continue + val idx = counters[i] + counters[i] = idx + 1 + if (site.ordinal < 0 || site.ordinal == idx) { + if (out == null) out = mutableListOf() + out.add(site) + } + } + return out ?: emptyList() + } + + /** 把 raw SiteSpec 喂给 MetadataResolver 三件套,得到带元数据的版本供 emitter 消费。 */ + private fun enrich(site: SiteSpec, anchor: Anchor, opcode: Int, anchorOwner: String, anchorDesc: String): SiteSpec { + var s = site + s = MetadataResolver.resolveDispatchContext(hostAccess, hostDesc, s) + s = MetadataResolver.resolveTrimReturnDescStreaming(anchor, opcode, anchorDesc, hostDesc, s) + s = MetadataResolver.resolveTrimSlot(hostAccess, hostDesc, s) + s = MetadataResolver.resolveBypassMetaStreaming(anchor, opcode, anchorOwner, anchorDesc, bypassTmpSlot, s) + return s + } + + private fun applySites( + matched: List, + isVoid: Boolean, + terminalConsumer: Boolean = false, + emitOriginal: () -> Unit, + ) { + var bypass: SiteSpec? = null + val before = mutableListOf() + val after = mutableListOf() + val trims = mutableListOf() + for (s in matched) { + when (s.kind) { + AdviceKind.GRAFT -> if (s.shift == Shift.AFTER) after += s else before += s + AdviceKind.BYPASS -> if (bypass == null) bypass = s + AdviceKind.TRIM -> trims += s + else -> { /* 其他 kind 由 AdviceAdapter 路径处理 */ } + } + } + for (s in before) { + DispatcherEmitter.buildGraft(s).accept(mv) + emittedCount++ + } + val b = bypass + if (b != null) { + DispatcherEmitter.buildBypass(b, isVoid).accept(mv) + emittedCount++ + } else if (terminalConsumer) { + // RETURN/THROW:trim 先于 emitOriginal —— 在终止消费指令之前拦截栈顶值 + for (s in trims) { + DispatcherEmitter.buildTrim(s).accept(mv) + emittedCount++ + } + emitOriginal() + } else { + emitOriginal() + for (s in trims) { + DispatcherEmitter.buildTrim(s).accept(mv) + emittedCount++ + } + } + for (s in after) { + DispatcherEmitter.buildGraft(s).accept(mv) + emittedCount++ + } + } + + // streaming 路径下,所有发射加起来的栈/局部冲击:dispatch 自身 +3、TRIM wide +4、BYPASS ALOAD tmp +1。 + // 取保守上界 +16/emission;下游 ClassWriter COMPUTE_MAXS 在最终写出时再精修。 + private var emittedCount: Int = 0 + + override fun visitMaxs(maxStack: Int, maxLocals: Int) { + // FrameVerifier 走 Analyzer,按下游 MethodNode.maxStack 预分配 Frame.values[], + // 我们 streaming 注入的 dispatch/TRIM/BYPASS 序列若超过原 maxStack 即 push 越界。直接抬到 + // max(原值 + 16*emission, 1024) 上界,避免 emission 计数估漏;ClassWriter COMPUTE_MAXS 不会 + // 因为更大的 maxStack 而产生更宽的 frame,仍按真实使用量精修最终值。 + val needed = maxOf(maxStack + 16 * emittedCount, if (emittedCount > 0) 1024 else maxStack) + val newLocals = maxOf(maxLocals, bypassTmpSlot + 1) + super.visitMaxs(maxOf(maxStack, needed), newLocals) + } + } + + /** + * Tree API 形态驱动:继承 [MethodNode](本身是 `MethodVisitor`,收完就是完整指令树), + * visitEnd 时跑 scan → match → plan → insert → accept(downstream)。 + * + * 相比旧 [RecordingDriver]: + * - 不再通过 InsertEmission 的 replay 路径回写字节码;发射产物直接是 [InsnList] + * - Label 以 [org.objectweb.asm.tree.LabelNode] 节点形式嵌入 `instructions`, + * ClassWriter 的 `COMPUTE_FRAMES` 能对其做正常推导,彻底消除原 recording 路径的 Frame.merge 崩溃 + * - 节点引用稳定,插入不影响其他 plan 的锚点,免去"倒序插入"技巧 + */ + private class TreeMethodDriver( + api: Int, + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array?, + private val downstream: MethodVisitor, + rawSites: List, + ) : MethodNode(api, access, name, descriptor, signature, exceptions) { + + // 与 streaming 路径 SiteVisitor 保持一致:防御性去重同一锚点+kind+target+shift+ordinal 完全相等的 + // SiteSpec 重复入队。Scalpel 累积路径曾观察到同一 advice 被注册多次,导致每锚点插入 N 份发射 → + // 对应 advice 命中计数被 ×N 放大(曾出现 expected=2 actual=6 的 3x 放大)。 + private val sites: List = rawSites.distinct() + + override fun visitEnd() { + super.visitEnd() + try { + runTreePipeline() + } catch (t: Throwable) { + taboolib.module.incision.diagnostic.Forensics.error( + "SiteWeaver tree pipeline failed: ${t::class.java.name}: ${t.message} " + + "| method=$name$desc sites=${sites.size}; fallback to raw accept", + t, + ) + } + this.accept(downstream) + } + + /** + * 扫遍 `instructions`,收集 [MatchEvent] → [EmissionPlan],按 plan 策略把 [InsnList] 插入。 + * + * 顺序说明:plans 按遇到锚点的顺序处理。同一锚点上多个 BEFORE plan,后加入者离 anchor 更近 + * (与 streaming 路径 `for (s in before) emitGraft(s)` 的语义一致);AFTER 同样。offset != 0 的 + * plan 落在邻居节点上,彼此不相冲。 + */ + private fun runTreePipeline() { + val plainSites = sites.filter { it.pattern !is SitePattern.OpcodeSeq } + val opcodeSeqSites = sites.filter { it.pattern is SitePattern.OpcodeSeq } + val invokeMatcher = InvokeMatcher(plainSites) + val fieldGetMatcher = FieldMatcher(plainSites, Anchor.FIELD_GET) + val fieldPutMatcher = FieldMatcher(plainSites, Anchor.FIELD_PUT) + val newMatcher = NewMatcher(plainSites) + val terminalMatcher = TerminalMatcher(plainSites) + + val opcodeSeqEntries: List = opcodeSeqSites.mapNotNull { s -> + val p = s.pattern + if (p is SitePattern.OpcodeSeq) OpcodeSeqMatcher.Entry(s, p.steps) else null + } + val opcodeSeqMatcher = OpcodeSeqMatcher(opcodeSeqEntries) + val planner = PlannerDispatcher() + + data class NodePlan(val anchor: AbstractInsnNode, val plan: EmissionPlan) + val plans = ArrayList() + + val nodes = instructions.toArray() + for (anchor in nodes) { + when (anchor) { + is MethodInsnNode -> for (ev in invokeMatcher.match(anchor.owner, anchor.name, anchor.desc)) + plans += NodePlan(anchor, planner.plan(ev)) + is FieldInsnNode -> { + val m = when (anchor.opcode) { + Opcodes.GETFIELD, Opcodes.GETSTATIC -> fieldGetMatcher + Opcodes.PUTFIELD, Opcodes.PUTSTATIC -> fieldPutMatcher + else -> null + } + if (m != null) for (ev in m.match(anchor.owner, anchor.name, anchor.desc)) + plans += NodePlan(anchor, planner.plan(ev)) + } + is TypeInsnNode -> if (anchor.opcode == Opcodes.NEW) for (ev in newMatcher.match(anchor.desc)) + plans += NodePlan(anchor, planner.plan(ev)) + is InsnNode -> for (ev in terminalMatcher.matchInsn(anchor.opcode)) + plans += NodePlan(anchor, planner.plan(ev)) + else -> Unit + } + } + + if (opcodeSeqEntries.isNotEmpty()) { + val indexedViews = buildRealInsnViews(nodes) + val seqEvents = opcodeSeqMatcher.match(indexedViews.views) + for (ev in seqEvents) { + val idx = indexedViews.indices.getOrNull(ev.anchorIndex) ?: continue + if (idx in nodes.indices) plans += NodePlan(nodes[idx], planner.plan(ev)) + } + } + + val headEvents = terminalMatcher.matchHead() + val headAnchor = findHeadInsertionAnchor(nodes) + + // 同 anchor 多 plan 的处理顺序:把 BYPASS 排到最后。 + // 原因:BYPASS 的实现是 insertBefore(anchor, em) + remove(anchor) —— 一旦先于 GRAFT AFTER + // 处理,anchor 就被移出 InsnList,后续 `instructions.insert(anchor, em)` 落到孤儿节点上 + // 静默失败,导致 GRAFT AFTER / TRIM 的 emission 丢失(曾观察到 surgeon-graft-invoke-after + // 命中 1 次而非期望的 2 次)。把 BYPASS 排最后后: + // 1) GRAFT BEFORE → 插在 anchor 之前 + // 2) GRAFT AFTER / TRIM → 插在 anchor 之后(成为 anchor 的兄弟节点) + // 3) BYPASS → insertBefore(anchor) + remove(anchor) —— 兄弟节点不受影响,最终顺序: + // ..., before-graft, bypass-emission, anchor-after-graft/trim, ... + // 同 anchor 内 BYPASS 仅取首个(applySites 流式路径已是该语义)。 + val sortedPlans = plans.sortedBy { if (it.plan.site.kind == AdviceKind.BYPASS) 1 else 0 } + for (np in sortedPlans) applyPlan(np.anchor, np.plan) + // 每个 emission 对栈的最大额外消耗:dispatch 本身 +3;TRIM RETURN wide DUP2_X2 路径 +4; + // BYPASS ASTORE/ALOAD tmp +1。取保守上界 +8 每个 site,避免 Analyzer/ClassWriter 在用旧 + // maxStack 初始化 Frame 时 push 越界(曾观察到 Frame.push: Insufficient maximum stack size + // 与 Frame.merge Index -1 out of bounds 两类失败)。COMPUTE_MAXS 仍会精修最终值。 + var insertedCount = plans.size + if (headEvents.isNotEmpty() && headAnchor != null) { + // HEAD 语义:方法最开头一次性插入;多条按声明顺序。 + // 但如果方法头已经有 AdviceInjector 生成的 whole-method @LEAD/@SPLICE 调度块, + // HEAD site 必须插在这段入口块之后,否则 HEAD @Trim 会先改写 args, + // 让 whole-method @Splice.resume.proceed() 看到的已是被污染后的参数。 + // 注意:必须经过 MetadataResolver 富集——TRIM ARG 的 trimIndex 在 SurgeonScanner 里是 + // 逻辑参数序号(0/1/2...),需要按宿主 access/desc 翻译成物理 LV slot,否则会 ASTORE + // 到 slot 0 覆盖 `this`,引发 PUTFIELD VerifyError。 + for (ev in headEvents) { + var site = ev.siteSpec + site = MetadataResolver.resolveDispatchContext(access, desc, site) + site = MetadataResolver.resolveTrimReturnDesc(headAnchor, desc, site) + site = MetadataResolver.resolveTrimSlot(access, desc, site) + val emission = buildEmission(site, isVoid = true) + instructions.insertBefore(headAnchor, emission) + insertedCount++ + } + } + if (insertedCount > 0) { + // FrameVerifier 走 Analyzer,按 m.maxStack 预分配 Frame.values[], + // push 越界即 IndexOutOfBoundsException。我们既不知道每个 emission 在该方法中的精确 + // 栈深度叠加,也不能让预检卡住——保守把 maxStack 抬到 1024 上界,ClassWriter 的 + // COMPUTE_MAXS 在最终写出时仍会精修到实际最小值。 + val needed = maxOf(this.maxStack + 16 * insertedCount, 1024) + if (needed > this.maxStack) this.maxStack = needed + // BYPASS 把 tmp slot 分配在 hostMaxLocals(第一个空闲槽),但 ASTORE 到该槽必须先把 + // MethodNode.maxLocals 抬上去,否则 Analyzer 立刻 "Trying to set an inexistant local variable N"。 + // 留 +2 兜底(ALOAD 2-slot 类型);COMPUTE_MAXS 仍会按真实使用量精修。 + val bypassPresent = sites.any { it.kind == AdviceKind.BYPASS } + if (bypassPresent) this.maxLocals = this.maxLocals + 2 + } + } + + private fun applyPlan(anchor: AbstractInsnNode, plan: EmissionPlan) { + var site = plan.site + site = MetadataResolver.resolveDispatchContext(access, desc, site) + site = MetadataResolver.resolveTrimReturnDesc(anchor, desc, site) + site = MetadataResolver.resolveTrimSlot(access, desc, site) + site = MetadataResolver.resolveBypassMeta(anchor, maxLocals, site) + val absOffset = kotlin.math.abs(site.offset) + val isVoid = isAnchorVoid(anchor) + when (plan.strategy) { + EmissionPlan.Strategy.IMMEDIATE -> insertAtAnchor(anchor, site, buildEmission(site, isVoid), isVoid) + EmissionPlan.Strategy.DEFERRED -> { + val emission = buildEmission(site, isVoid) + val target = skipForward(anchor, absOffset) + instructions.insert(target, emission) + } + EmissionPlan.Strategy.BUFFERING -> { + val emission = buildEmission(site, isVoid) + val target = skipBackward(anchor, absOffset) + instructions.insertBefore(target, emission) + } + } + } + + /** + * IMMEDIATE 就地插入: + * - GRAFT: shift=AFTER 插入 anchor 之后,否则之前 + * - TRIM: 默认插入 anchor 之后(在锚点产生的栈顶值上做拦截);当 anchor 是 RETURN/ATHROW + * 这类**消费栈顶**的终止指令时,必须改为插入之前——否则 emission 落在 unreachable + * 代码区,DUP 立刻 "Cannot pop operand off an empty stack" + * - BYPASS: 用发射的 InsnList 替换 anchor 原指令 + * - 其他: 插入之前(SPLICE/EXCISE 目前不会进 recording 路径,兜底 BEFORE) + */ + private fun insertAtAnchor(anchor: AbstractInsnNode, site: SiteSpec, em: InsnList, isVoid: Boolean) { + when (site.kind) { + AdviceKind.GRAFT -> if (site.shift == Shift.AFTER) instructions.insert(anchor, em) + else instructions.insertBefore(anchor, em) + AdviceKind.TRIM -> { + if (isTerminalConsumer(anchor)) instructions.insertBefore(anchor, em) + else instructions.insert(anchor, em) + } + AdviceKind.BYPASS -> { + val afterOriginal = LabelNode() + instructions.insertBefore(anchor, DispatcherEmitter.buildConditionalBypass(site, isVoid, afterOriginal)) + instructions.insert(anchor, afterOriginal) + } + else -> instructions.insertBefore(anchor, em) + } + } + + private fun buildEmission(site: SiteSpec, isVoid: Boolean): InsnList = when (site.kind) { + AdviceKind.GRAFT -> DispatcherEmitter.buildGraft(site) + AdviceKind.BYPASS -> DispatcherEmitter.buildBypass(site, isVoid) + AdviceKind.TRIM -> DispatcherEmitter.buildTrim(site) + else -> DispatcherEmitter.buildGraft(site) + } + + private fun isAnchorVoid(n: AbstractInsnNode): Boolean = when (n) { + is MethodInsnNode -> n.desc.endsWith(")V") + is FieldInsnNode -> n.opcode == Opcodes.PUTFIELD || n.opcode == Opcodes.PUTSTATIC + is InsnNode -> n.opcode == Opcodes.ATHROW || n.opcode in Opcodes.IRETURN..Opcodes.RETURN + else -> false + } + + /** + * 终止消费者:anchor 自身**消费**栈顶值(IRETURN..ARETURN / ATHROW)。 + * TRIM 这类需要先于栈顶值消费做拦截的 emission,必须插在这种 anchor **之前**, + * 否则插到之后即落入 unreachable 区,Analyzer 视作空栈,DUP/SWAP 全部立即崩。 + * 注意 RETURN(void)也属于消费者,但栈顶 = 空,TRIM 在 void 场景下无意义。 + */ + private fun isTerminalConsumer(n: AbstractInsnNode): Boolean = n is InsnNode && ( + n.opcode == Opcodes.ATHROW || n.opcode in Opcodes.IRETURN..Opcodes.ARETURN + ) + + /** 向前越过 n 条真实指令(opcode >= 0 排除 Label/Frame/LineNumber)。 */ + private fun skipForward(from: AbstractInsnNode, n: Int): AbstractInsnNode { + if (n <= 0) return from + var cur: AbstractInsnNode = from + var left = n + while (cur.next != null && left > 0) { + cur = cur.next + if (cur.opcode >= 0) left-- + } + return cur + } + + private fun skipBackward(from: AbstractInsnNode, n: Int): AbstractInsnNode { + if (n <= 0) return from + var cur: AbstractInsnNode = from + var left = n + while (cur.previous != null && left > 0) { + cur = cur.previous + if (cur.opcode >= 0) left-- + } + return cur + } + + private data class IndexedInsnViews( + val indices: List, + val views: List, + ) + + private fun buildRealInsnViews(nodes: Array): IndexedInsnViews { + val indices = ArrayList(nodes.size) + val views = ArrayList(nodes.size) + for ((index, node) in nodes.withIndex()) { + val view = toInsnView(node) ?: continue + indices += index + views += view + } + return IndexedInsnViews(indices, views) + } + + private fun toInsnView(n: AbstractInsnNode): OpcodeSeqMatcher.InsnView? { + return when (n) { + is InsnNode -> OpcodeSeqMatcher.InsnView(n.opcode) + is IntInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode) + is VarInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode) + is TypeInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode, owner = n.desc) + is FieldInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode, n.owner, n.name, n.desc) + is MethodInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode, n.owner, n.name, n.desc) + is LdcInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.LDC, cst = n.cst) + is JumpInsnNode -> OpcodeSeqMatcher.InsnView(n.opcode) + is IincInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.IINC) + is TableSwitchInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.TABLESWITCH) + is LookupSwitchInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.LOOKUPSWITCH) + is MultiANewArrayInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.MULTIANEWARRAY, owner = n.desc) + is InvokeDynamicInsnNode -> OpcodeSeqMatcher.InsnView(Opcodes.INVOKEDYNAMIC, name = n.name, descriptor = n.desc) + else -> null + } + } + + private fun findHeadInsertionAnchor(nodes: Array): AbstractInsnNode? { + var anchor = nodes.firstOrNull { it.opcode >= 0 } ?: return null + while (true) { + val next = skipGeneratedEntryDispatch(anchor) ?: break + anchor = next + } + return anchor + } + + private fun skipGeneratedEntryDispatch(start: AbstractInsnNode): AbstractInsnNode? { + if (start !is LdcInsnNode || start.cst !is String) return null + var cursor: AbstractInsnNode? = start + var scannedReal = 0 + var dispatchCall: MethodInsnNode? = null + while (cursor != null && scannedReal < 48) { + if (cursor.opcode >= 0) scannedReal++ + if (cursor is MethodInsnNode && + cursor.owner == "io/izzel/incision/bridge/IncisionBridge" && + cursor.name == "dispatch" + ) { + dispatchCall = cursor + break + } + cursor = cursor.next + } + val call = dispatchCall ?: return null + val nextReal = nextRealInsn(call.next) ?: return null + if (nextReal is InsnNode && nextReal.opcode == Opcodes.POP) { + return nextRealInsn(nextReal.next) + } + if (nextReal is InsnNode && nextReal.opcode == Opcodes.DUP) { + val ifNull = nextRealInsn(nextReal.next) as? JumpInsnNode ?: return null + if (ifNull.opcode != Opcodes.IFNULL) return null + var cursorAfterLabel: AbstractInsnNode? = ifNull.label + while (cursorAfterLabel != null) { + if (cursorAfterLabel is InsnNode && cursorAfterLabel.opcode == Opcodes.POP) { + return nextRealInsn(cursorAfterLabel.next) + } + cursorAfterLabel = cursorAfterLabel.next + } + } + return null + } + + private fun nextRealInsn(from: AbstractInsnNode?): AbstractInsnNode? { + var cursor = from + while (cursor != null && cursor.opcode < 0) cursor = cursor.next + return cursor + } + } + + /** + * recording 路径驱动:继承 [RecordingMethodVisitor](next=null,禁双发),visitEnd 时运行 + * scan → match → plan → insert → replay(downstream)。 + */ + private class RecordingDriver( + api: Int, + private val downstream: MethodVisitor, + private val sites: List, + ) : RecordingMethodVisitor(api, next = null) { + + override fun visitEnd() { + super.visitEnd() + try { + runPipeline() + } catch (t: Throwable) { + taboolib.module.incision.diagnostic.Forensics.error( + "SiteWeaver recording pipeline failed: ${t::class.java.name}: ${t.message} | sites=${sites.size} actions=${actions.size}; fallback to raw replay", + t, + ) + Replayer(actions).replay(downstream) + } + } + + private fun runPipeline() { + val actions = this.actions + val replayer = Replayer(actions) + val planner = PlannerDispatcher() + + val plainSites = sites.filter { it.pattern !is SitePattern.OpcodeSeq } + val opcodeSeqSites = sites.filter { it.pattern is SitePattern.OpcodeSeq } + val invokeMatcher = InvokeMatcher(plainSites) + val fieldGetMatcher = FieldMatcher(plainSites, Anchor.FIELD_GET) + val fieldPutMatcher = FieldMatcher(plainSites, Anchor.FIELD_PUT) + val newMatcher = NewMatcher(plainSites) + val terminalMatcher = TerminalMatcher(plainSites) + + val opcodeSeqEntries: List = opcodeSeqSites.mapNotNull { s -> + val p = s.pattern + if (p is SitePattern.OpcodeSeq) OpcodeSeqMatcher.Entry(s, p.steps) else null + } + val opcodeSeqMatcher = OpcodeSeqMatcher(opcodeSeqEntries) + + data class IndexedPlan(val index: Int, val plan: EmissionPlan) + val indexedPlans = ArrayList() + + fun addPlans(anchorIdx: Int, events: List) { + for (ev in events) { + val evWithIdx = if (ev.anchorIndex >= 0) ev else MatchEvent(ev.siteSpec, anchorIdx) + indexedPlans += IndexedPlan(anchorIdx, planner.plan(evWithIdx)) + } + } + + actions.forEachIndexed { idx, a -> + when (a) { + is InsnAction.MethodInsn -> addPlans(idx, invokeMatcher.match(a.owner, a.name, a.descriptor)) + is InsnAction.FieldInsn -> { + val m = when (a.opcode) { + Opcodes.GETFIELD, Opcodes.GETSTATIC -> fieldGetMatcher + Opcodes.PUTFIELD, Opcodes.PUTSTATIC -> fieldPutMatcher + else -> null + } + if (m != null) addPlans(idx, m.match(a.owner, a.name, a.descriptor)) + } + is InsnAction.TypeInsn -> if (a.opcode == Opcodes.NEW) { + addPlans(idx, newMatcher.match(a.type)) + } + is InsnAction.Insn -> addPlans(idx, terminalMatcher.matchInsn(a.opcode)) + else -> Unit + } + } + + if (opcodeSeqEntries.isNotEmpty()) { + val indexedViews = buildRealInsnViews(actions) + val seqEvents = opcodeSeqMatcher.match(indexedViews.views) + for (ev in seqEvents) { + val idx = indexedViews.indices.getOrNull(ev.anchorIndex) ?: continue + indexedPlans += IndexedPlan(idx, planner.plan(ev.copy(anchorIndex = idx))) + } + } + + val headEvents = terminalMatcher.matchHead() + val headInsertIdx = firstRealInsnIndex(actions) + + // 倒序插入避免索引漂移 + val sortedPlans = indexedPlans.sortedByDescending { it.index } + for (ip in sortedPlans) { + applyPlan(replayer, ip.index, ip.plan, actions) + } + if (headEvents.isNotEmpty() && headInsertIdx >= 0) { + for (ev in headEvents.reversed()) { + val emission = toEmission(ev.siteSpec, isVoid = true) + replayer.insertBefore(headInsertIdx, emission) + } + } + + replayer.replay(downstream) + } + + /** + * 按策略把 [InsnAction.InsertEmission] 写入 replayer。 + * - IMMEDIATE:原位(site.shift 决定 before/after,offset == 0) + * - DEFERRED:锚点 + offset 条真实指令后 + * - BUFFERING:锚点 - offset 条真实指令前(OpcodeSeq 命中也走这条) + */ + private fun applyPlan( + replayer: Replayer, + anchorIdx: Int, + plan: EmissionPlan, + actions: List, + ) { + val site = plan.site + val absOffset = kotlin.math.abs(site.offset) + val isVoid = isAnchorVoid(actions.getOrNull(anchorIdx)) + val emission = toEmission(site, isVoid) + when (plan.strategy) { + EmissionPlan.Strategy.IMMEDIATE -> insertAtAnchor(replayer, anchorIdx, site, emission) + EmissionPlan.Strategy.DEFERRED -> { + val target = skipForward(actions, anchorIdx, absOffset) + replayer.insertAfter(target, emission) + } + EmissionPlan.Strategy.BUFFERING -> { + val target = skipBackward(actions, anchorIdx, absOffset) + replayer.insertBefore(target, emission) + } + } + } + + private fun insertAtAnchor(replayer: Replayer, anchorIdx: Int, site: SiteSpec, emission: InsnAction) { + when (site.kind) { + AdviceKind.GRAFT -> if (site.shift == Shift.AFTER) { + replayer.insertAfter(anchorIdx, emission) + } else { + replayer.insertBefore(anchorIdx, emission) + } + AdviceKind.TRIM -> replayer.insertAfter(anchorIdx, emission) + else -> replayer.insertBefore(anchorIdx, emission) + } + } + + private fun toEmission(site: SiteSpec, isVoid: Boolean): InsnAction.InsertEmission = + InsnAction.InsertEmission(site.kind, site, isVoid) + + private fun isAnchorVoid(a: InsnAction?): Boolean = when (a) { + is InsnAction.MethodInsn -> a.descriptor.endsWith(")V") + is InsnAction.FieldInsn -> a.opcode == Opcodes.PUTFIELD || a.opcode == Opcodes.PUTSTATIC + is InsnAction.Insn -> a.opcode == Opcodes.ATHROW || a.opcode in Opcodes.IRETURN..Opcodes.RETURN + else -> false + } + + private fun skipForward(actions: List, from: Int, n: Int): Int { + if (n <= 0) return from + var i = from + var left = n + while (i + 1 < actions.size && left > 0) { + i++ + if (isRealInsn(actions[i])) left-- + } + return i + } + + private fun skipBackward(actions: List, from: Int, n: Int): Int { + if (n <= 0) return from + var i = from + var left = n + while (i > 0 && left > 0) { + i-- + if (isRealInsn(actions[i])) left-- + } + return i + } + + private fun firstRealInsnIndex(actions: List): Int { + for ((i, a) in actions.withIndex()) if (isRealInsn(a)) return i + return -1 + } + + private fun isRealInsn(a: InsnAction): Boolean = when (a) { + is InsnAction.Insn, is InsnAction.IntInsn, is InsnAction.VarInsn, + is InsnAction.TypeInsn, is InsnAction.FieldInsn, is InsnAction.MethodInsn, + is InsnAction.InvokeDynamicInsn, is InsnAction.JumpInsn, is InsnAction.LdcInsn, + is InsnAction.IincInsn, is InsnAction.TableSwitchInsn, is InsnAction.LookupSwitchInsn, + is InsnAction.MultiANewArrayInsn -> true + else -> false + } + + private data class IndexedActionViews( + val indices: List, + val views: List, + ) + + private fun buildRealInsnViews(actions: List): IndexedActionViews { + val indices = ArrayList(actions.size) + val views = ArrayList(actions.size) + for ((index, action) in actions.withIndex()) { + val view = toInsnView(action) ?: continue + indices += index + views += view + } + return IndexedActionViews(indices, views) + } + + private fun toInsnView(a: InsnAction): OpcodeSeqMatcher.InsnView? { + return when (a) { + is InsnAction.Insn -> OpcodeSeqMatcher.InsnView(a.opcode) + is InsnAction.IntInsn -> OpcodeSeqMatcher.InsnView(a.opcode) + is InsnAction.VarInsn -> OpcodeSeqMatcher.InsnView(a.opcode) + is InsnAction.TypeInsn -> OpcodeSeqMatcher.InsnView(a.opcode, owner = a.type) + is InsnAction.FieldInsn -> OpcodeSeqMatcher.InsnView(a.opcode, a.owner, a.name, a.descriptor) + is InsnAction.MethodInsn -> OpcodeSeqMatcher.InsnView(a.opcode, a.owner, a.name, a.descriptor) + is InsnAction.LdcInsn -> OpcodeSeqMatcher.InsnView(Opcodes.LDC, cst = a.cst) + is InsnAction.JumpInsn -> OpcodeSeqMatcher.InsnView(a.opcode) + is InsnAction.IincInsn -> OpcodeSeqMatcher.InsnView(Opcodes.IINC) + is InsnAction.TableSwitchInsn -> OpcodeSeqMatcher.InsnView(Opcodes.TABLESWITCH) + is InsnAction.LookupSwitchInsn -> OpcodeSeqMatcher.InsnView(Opcodes.LOOKUPSWITCH) + is InsnAction.MultiANewArrayInsn -> OpcodeSeqMatcher.InsnView(Opcodes.MULTIANEWARRAY, owner = a.descriptor) + is InsnAction.InvokeDynamicInsn -> OpcodeSeqMatcher.InsnView(Opcodes.INVOKEDYNAMIC, name = a.name, descriptor = a.descriptor) + else -> null + } + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/SiteSpec.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/SiteSpec.kt new file mode 100644 index 000000000..68b770c68 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/SiteSpec.kt @@ -0,0 +1,64 @@ +package taboolib.module.incision.weaver.site + +import taboolib.module.incision.annotation.Trim +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.api.Shift +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * 锚点描述 — 由 weaver 内部使用,对应 [taboolib.module.incision.annotation.Site] 注解。 + * + * @property anchor 锚点类型 (INVOKE / FIELD_GET / FIELD_PUT / NEW / THROW / RETURN / HEAD / TAIL) + * @property ownerPattern 锚点目标的 owner(internalName);空则匹配所有 + * @property namePattern 锚点目标的 name;空则匹配所有 + * @property descPattern 锚点目标的 desc;空则匹配所有 + * @property shift 相对锚点的偏移(BEFORE / AFTER) + * @property ordinal 第 N 次出现(0 起步),-1 表示全部 + * @property offset 沿 [shift] 方向跨越的指令条数;0 表示就在锚点处。 + * @property pattern 可选的高阶模式(来自 [SitePattern],例如 [SitePattern.OpcodeSeq])。 + * 旧的三字段 ownerPattern/namePattern/descPattern 仍保留以兼容 streaming 后端。 + * @property trimKind kind=TRIM 时区分 RETURN / ARG / VAR 三种语义;其他 kind 忽略。 + * @property trimIndex ARG/VAR 模式下的实参索引或局部变量槽。 + * @property trimArgDescriptor ARG/VAR 模式下被改写的值的 JVM 描述符(用于 CHECKCAST),如 "Ljava/lang/String;"。 + * @property trimReturnDescriptor RETURN 模式下被改写的栈顶值的 JVM 描述符(INVOKE 返回类型 / FIELD_GET 字段类型 / + * RETURN 锚点宿主方法返回类型)。SurgeonScanner 在能从 [descPattern] / [target] 静态推断时预填;否则由 + * [taboolib.module.incision.weaver.SiteWeaver] 在 applyPlan 阶段按 anchor 节点补齐。空串时 emitter 走兼容 + * 的"按引用 1-slot 处理"老路径。 + * @property bypassConsumedDescs BYPASS 模式下,原锚点指令在执行前消费的栈值描述符的拼接(栈底→栈顶顺序), + * 例如 INVOKEVIRTUAL Lowner;ID 表示栈底是 receiver、然后 I、然后 D(双宽)。空串表示不消费。 + * 由 [taboolib.module.incision.weaver.SiteWeaver] 在 applyPlan 阶段按 anchor 节点填补。 + * @property bypassReturnDescriptor BYPASS 模式下,原锚点指令产生的栈顶值描述符(INVOKE 返回 / FIELD_GET 字段 / + * PUTFIELD/PUTSTATIC 为 "V")。空串时 emitter 退回旧行为(按 [isVoid] 兼容路径)。 + * @property bypassTempSlot BYPASS 模式下保存 dispatch 返回 Object 的临时局部变量槽位;由 SiteWeaver 用 + * 宿主方法的 maxLocals 分配。-1 表示未分配。 + * @property adviceId 对应 [taboolib.module.incision.runtime.AdviceEntry.id]。由 SurgeonScanner 在构造 AdviceEntry + * 时一并写入;DispatcherEmitter 把它编码进 dispatch 调用的 targetSig 后缀(`baseSig#adviceId`), + * [taboolib.module.incision.runtime.TheatreDispatcher.dispatch] 按此精确路由到单一 AdviceEntry。 + * 空串表示无 id(兼容早期 emission,dispatch 退化为按 phase 匹配;但多 advice 共目标会导致全员触发)。 + * @property hostMethodDescriptor 宿主方法的 JVM 描述符。site dispatch 需要它来回填原方法 args。 + * @property hostIsStatic 宿主方法是否为静态方法;为 false 时 site dispatch 会把 slot0 作为 `self` 传入。 + */ +data class SiteSpec( + val anchor: Anchor, + val ownerPattern: String = "", + val namePattern: String = "", + val descPattern: String = "", + val shift: Shift = Shift.BEFORE, + val ordinal: Int = -1, + val kind: AdviceKind, + val target: MethodCoordinate, + val offset: Int = 0, + val pattern: SitePattern? = null, + val trimKind: Trim.Kind? = null, + val trimIndex: Int = 0, + val trimArgDescriptor: String = "", + val trimReturnDescriptor: String = "", + val bypassConsumedDescs: String = "", + val bypassReturnDescriptor: String = "", + val bypassTempSlot: Int = -1, + val adviceId: String = "", + val hostMethodDescriptor: String = "", + val hostIsStatic: Boolean = false, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/emitter/DispatcherEmitter.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/emitter/DispatcherEmitter.kt new file mode 100644 index 000000000..0c3d5727d --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/emitter/DispatcherEmitter.kt @@ -0,0 +1,448 @@ +package taboolib.module.incision.weaver.site.emitter + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.LabelNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode +import taboolib.module.incision.annotation.Trim +import taboolib.module.incision.runtime.DispatchSignatureCodec +import taboolib.module.incision.weaver.site.SiteSpec + +/** + * dispatcher 字节码发射工具。 + * + * 同时提供两种形态: + * - 旧:`emit*(mv: MethodVisitor, site)` — 直接向 `MethodVisitor` 发射(streaming 路径 & 旧 recording 路径用) + * - 新:`build*(site): InsnList` — 返回 [InsnList],供 Tree API 重写路径直接 insertBefore/insert 到 [org.objectweb.asm.tree.MethodNode.instructions] + * + * 旧方法委托到新方法 + 一次 `list.accept(mv)` 回放以保持行为一致,避免逻辑双维护。 + * + * 所有 Graft/Bypass/Trim 最终都调 `IncisionBridge.dispatch(sig, self, args)`。 + */ +object DispatcherEmitter { + + private const val BRIDGE = "io/izzel/incision/bridge/IncisionBridge" + private const val DISPATCH_DESC = "(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;" + + /** + * 生成 dispatch 调用的 targetSig。 + * + * 格式:`owner.name(desc)return` 或带 adviceId 后缀 `owner.name(desc)return#adviceId`。 + * 后者由 [TheatreDispatcher.dispatch] 按 adviceId 精确路由到单一 AdviceEntry,避免多 advice + * 共目标(同 chain 内多条 GRAFT/BYPASS/TRIM)时按 phase 匹配误触发全部 entry 的放大问题。 + * 空 adviceId 时退回旧的 phase-only 路径,保留对历史 emission 的兼容。 + */ + fun sigOf(site: SiteSpec): String { + val base = "${site.target.owner}.${site.target.name}${site.target.descriptor}" + return DispatchSignatureCodec.compose(base, site.adviceId) + } + + // ---- Tree API (推荐) --------------------------------------------------- + + /** + * 发射 dispatcher 调用,栈顶留下 Object 返回值。 + * + * 产物序列:`LDC sig · self · args[] · INVOKESTATIC dispatch` + */ + fun buildDispatch(site: SiteSpec): InsnList { + val list = InsnList() + list.add(LdcInsnNode(sigOf(site))) + emitSelfLoad(list, site) + emitArgsArray(list, site) + list.add(MethodInsnNode(Opcodes.INVOKESTATIC, BRIDGE, "dispatch", DISPATCH_DESC, false)) + return list + } + + /** GRAFT:发 dispatcher 并丢弃返回值。 */ + fun buildGraft(site: SiteSpec): InsnList { + val list = buildDispatch(site) + list.add(InsnNode(Opcodes.POP)) + return list + } + + fun buildBypassDispatch(site: SiteSpec): InsnList { + val list = InsnList() + list.add(LdcInsnNode(sigOf(site))) + emitSelfLoad(list, site) + emitArgsArray(list, site) + list.add(MethodInsnNode(Opcodes.INVOKESTATIC, BRIDGE, "dispatchBypass", DISPATCH_DESC, false)) + return list + } + + /** + * BYPASS:用 dispatcher 调用替代原指令。 + * + * 流程([SiteSpec.bypassConsumedDescs] / [SiteSpec.bypassReturnDescriptor] 已由 SiteWeaver 填好时): + * 1. dispatch → 栈顶 Object + * 2. ASTORE tmp + * 3. 倒序 POP/POP2 弹掉原指令本应消费的栈值 + * 4. ALOAD tmp + * 5. 按 [SiteSpec.bypassReturnDescriptor] 把 Object 转成期望类型:void → POP;引用 → CHECKCAST;primitive → CHECKCAST + unbox + * + * 元数据缺失时退回兼容旧行为:dispatch + (isVoid 时)POP。 + */ + fun buildBypass(site: SiteSpec, isVoid: Boolean): InsnList { + val tmp = site.bypassTempSlot + val retDesc = site.bypassReturnDescriptor + if (tmp < 0 || retDesc.isEmpty()) { + // 旧兼容路径 + val list = buildDispatch(site) + if (isVoid) list.add(InsnNode(Opcodes.POP)) + return list + } + val list = buildDispatch(site) + list.add(VarInsnNode(Opcodes.ASTORE, tmp)) + emitPopConsumed(list, site.bypassConsumedDescs) + when { + retDesc == "V" -> { /* 不再 ALOAD,原指令本来就不产值 */ } + retDesc.startsWith("L") || retDesc.startsWith("[") -> { + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + val castTarget = when { + retDesc.startsWith("L") && retDesc.endsWith(";") -> retDesc.substring(1, retDesc.length - 1) + else -> retDesc + } + if (castTarget != "java/lang/Object") { + list.add(TypeInsnNode(Opcodes.CHECKCAST, castTarget)) + } + } + else -> { + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + addUnbox(list, retDesc) + } + } + return list + } + + /** 按 [descs](栈底→栈顶顺序拼接的 JVM 描述符)从栈顶到栈底依次 POP/POP2。 */ + private fun emitPopConsumed(list: InsnList, descs: String) { + if (descs.isEmpty()) return + val parsed = parseConcatenatedDescs(descs) + // 倒序:先弹栈顶 + for (i in parsed.indices.reversed()) { + val d = parsed[i] + if (d == "J" || d == "D") list.add(InsnNode(Opcodes.POP2)) + else list.add(InsnNode(Opcodes.POP)) + } + } + + /** 拆分诸如 `Lowner;ID[Ljava/lang/String;` 这样的拼接描述符串。 */ + private fun parseConcatenatedDescs(s: String): List { + val out = mutableListOf() + var i = 0 + while (i < s.length) { + val start = i + while (i < s.length && s[i] == '[') i++ + when (s.getOrNull(i)) { + 'L' -> { + val semi = s.indexOf(';', i) + if (semi < 0) return out + out += s.substring(start, semi + 1); i = semi + 1 + } + null -> return out + else -> { out += s.substring(start, i + 1); i++ } + } + } + return out + } + + /** + * TRIM 调度入口 —— 按 [SiteSpec.trimKind] 分发: + * - RETURN(默认):栈前 [origVal];调 dispatcher 后非 null 替换;栈后 [origVal | dispatchRet] + * - ARG / VAR:与栈无关,从 LV slot 读 → 调 dispatcher → 非 null CHECKCAST + 写回 slot + */ + fun buildTrim(site: SiteSpec): InsnList = when (site.trimKind) { + Trim.Kind.ARG, Trim.Kind.VAR -> buildTrimSlot(site) + Trim.Kind.RETURN, null -> buildTrimReturn(site) + } + + private fun buildTrimReturn(site: SiteSpec): InsnList { + val desc = site.trimReturnDescriptor + return when { + desc.isEmpty() || desc.startsWith("L") || desc.startsWith("[") -> buildTrimReturnReference(desc) + desc == "V" -> InsnList() + desc == "J" || desc == "D" -> buildTrimReturnWide(desc) + else -> buildTrimReturnNarrow(desc) + }.let { tail -> + val out = buildDispatch(site) + out.add(tail) + out + } + } + + /** 引用类型 / 未知:栈前 [orig:R],栈后 [orig|ret-as-R]。 */ + private fun buildTrimReturnReference(desc: String): InsnList { + val list = InsnList() + val keep = LabelNode() + val end = LabelNode() + list.add(InsnNode(Opcodes.DUP)) + list.add(JumpInsnNode(Opcodes.IFNULL, keep)) + val castTarget = when { + desc.startsWith("L") && desc.endsWith(";") -> desc.substring(1, desc.length - 1) + desc.startsWith("[") -> desc + else -> "" + } + if (castTarget.isNotEmpty() && castTarget != "java/lang/Object") { + list.add(TypeInsnNode(Opcodes.CHECKCAST, castTarget)) + } + list.add(InsnNode(Opcodes.SWAP)) + list.add(InsnNode(Opcodes.POP)) + list.add(JumpInsnNode(Opcodes.GOTO, end)) + list.add(keep) + list.add(InsnNode(Opcodes.POP)) + list.add(end) + return list + } + + /** 1-slot primitive (I/S/B/C/Z/F):栈前 [orig:1],栈后 [orig|unboxed:1]。 */ + private fun buildTrimReturnNarrow(desc: String): InsnList { + val list = InsnList() + val keep = LabelNode() + val end = LabelNode() + list.add(InsnNode(Opcodes.DUP)) + list.add(JumpInsnNode(Opcodes.IFNULL, keep)) + // [orig:1, ret] → 用 unbox 出的 primitive 替换 orig + addUnbox(list, desc) + // 此时栈 [orig:1, prim:1] + list.add(InsnNode(Opcodes.SWAP)) + list.add(InsnNode(Opcodes.POP)) + list.add(JumpInsnNode(Opcodes.GOTO, end)) + list.add(keep) + list.add(InsnNode(Opcodes.POP)) // 弹掉 null dispatch 结果,保留 orig + list.add(end) + return list + } + + /** 2-slot primitive (J/D):栈前 [orig:2],栈后 [orig:2|unboxed:2]。 */ + private fun buildTrimReturnWide(desc: String): InsnList { + val list = InsnList() + val keep = LabelNode() + val end = LabelNode() + list.add(InsnNode(Opcodes.DUP)) + list.add(JumpInsnNode(Opcodes.IFNULL, keep)) + // [orig:2, ret] → unbox → [orig:2, prim:2] + addUnbox(list, desc) + // pop orig:2,留 prim:2 + list.add(InsnNode(Opcodes.DUP2_X2)) // [prim:2, orig:2, prim:2] + list.add(InsnNode(Opcodes.POP2)) // [prim:2, orig:2] + list.add(InsnNode(Opcodes.POP2)) // [prim:2] + list.add(JumpInsnNode(Opcodes.GOTO, end)) + list.add(keep) + list.add(InsnNode(Opcodes.POP)) // 弹掉 null:[orig:2] + list.add(end) + return list + } + + /** + * 把栈顶的 Object 替换为 primitive:CHECKCAST → invokeVirtual XxxValue。 + * I→intValue, S→shortValue, B→byteValue, C→charValue, Z→booleanValue, F→floatValue, + * J→longValue, D→doubleValue。 + */ + private fun addUnbox(list: InsnList, desc: String) { + val (boxed, method, ret) = when (desc) { + "I" -> Triple("java/lang/Integer", "intValue", "()I") + "S" -> Triple("java/lang/Short", "shortValue", "()S") + "B" -> Triple("java/lang/Byte", "byteValue", "()B") + "C" -> Triple("java/lang/Character", "charValue", "()C") + "Z" -> Triple("java/lang/Boolean", "booleanValue", "()Z") + "F" -> Triple("java/lang/Float", "floatValue", "()F") + "J" -> Triple("java/lang/Long", "longValue", "()J") + "D" -> Triple("java/lang/Double", "doubleValue", "()D") + else -> return + } + list.add(TypeInsnNode(Opcodes.CHECKCAST, boxed)) + list.add(MethodInsnNode(Opcodes.INVOKEVIRTUAL, boxed, method, ret, false)) + } + + private fun emitSelfLoad(list: InsnList, site: SiteSpec) { + if (site.hostMethodDescriptor.isBlank() || site.hostIsStatic) { + list.add(InsnNode(Opcodes.ACONST_NULL)) + } else { + list.add(VarInsnNode(Opcodes.ALOAD, 0)) + } + } + + private fun emitArgsArray(list: InsnList, site: SiteSpec) { + val hostDesc = site.hostMethodDescriptor + if (hostDesc.isBlank()) { + list.add(InsnNode(Opcodes.ICONST_0)) + list.add(TypeInsnNode(Opcodes.ANEWARRAY, "java/lang/Object")) + return + } + val args = Type.getArgumentTypes(hostDesc) + pushInt(list, args.size) + list.add(TypeInsnNode(Opcodes.ANEWARRAY, "java/lang/Object")) + var slot = if (site.hostIsStatic) 0 else 1 + for ((index, type) in args.withIndex()) { + list.add(InsnNode(Opcodes.DUP)) + pushInt(list, index) + list.add(loadVarInsn(type, slot)) + boxIfNeeded(list, type) + list.add(InsnNode(Opcodes.AASTORE)) + slot += type.size + } + } + + private fun pushInt(list: InsnList, value: Int) { + when (value) { + -1 -> list.add(InsnNode(Opcodes.ICONST_M1)) + 0 -> list.add(InsnNode(Opcodes.ICONST_0)) + 1 -> list.add(InsnNode(Opcodes.ICONST_1)) + 2 -> list.add(InsnNode(Opcodes.ICONST_2)) + 3 -> list.add(InsnNode(Opcodes.ICONST_3)) + 4 -> list.add(InsnNode(Opcodes.ICONST_4)) + 5 -> list.add(InsnNode(Opcodes.ICONST_5)) + in Byte.MIN_VALUE..Byte.MAX_VALUE -> list.add(org.objectweb.asm.tree.IntInsnNode(Opcodes.BIPUSH, value)) + in Short.MIN_VALUE..Short.MAX_VALUE -> list.add(org.objectweb.asm.tree.IntInsnNode(Opcodes.SIPUSH, value)) + else -> list.add(LdcInsnNode(value)) + } + } + + private fun loadVarInsn(type: Type, slot: Int): VarInsnNode = VarInsnNode( + when (type.sort) { + Type.BOOLEAN, Type.BYTE, Type.CHAR, Type.SHORT, Type.INT -> Opcodes.ILOAD + Type.FLOAT -> Opcodes.FLOAD + Type.LONG -> Opcodes.LLOAD + Type.DOUBLE -> Opcodes.DLOAD + else -> Opcodes.ALOAD + }, + slot, + ) + + private fun boxIfNeeded(list: InsnList, type: Type) { + when (type.sort) { + Type.BOOLEAN -> boxPrimitive(list, "java/lang/Boolean", "(Z)Ljava/lang/Boolean;") + Type.BYTE -> boxPrimitive(list, "java/lang/Byte", "(B)Ljava/lang/Byte;") + Type.CHAR -> boxPrimitive(list, "java/lang/Character", "(C)Ljava/lang/Character;") + Type.SHORT -> boxPrimitive(list, "java/lang/Short", "(S)Ljava/lang/Short;") + Type.INT -> boxPrimitive(list, "java/lang/Integer", "(I)Ljava/lang/Integer;") + Type.FLOAT -> boxPrimitive(list, "java/lang/Float", "(F)Ljava/lang/Float;") + Type.LONG -> boxPrimitive(list, "java/lang/Long", "(J)Ljava/lang/Long;") + Type.DOUBLE -> boxPrimitive(list, "java/lang/Double", "(D)Ljava/lang/Double;") + else -> Unit + } + } + + private fun boxPrimitive(list: InsnList, owner: String, desc: String) { + list.add(MethodInsnNode(Opcodes.INVOKESTATIC, owner, "valueOf", desc, false)) + } + + private fun buildTrimSlot(site: SiteSpec): InsnList { + val list = InsnList() + val slot = site.trimIndex + val argDesc = site.trimArgDescriptor + val skip = LabelNode() + val end = LabelNode() + list.add(buildDispatch(site)) // [ret] + list.add(InsnNode(Opcodes.DUP)) // [ret, ret] + list.add(JumpInsnNode(Opcodes.IFNULL, skip)) // 消费 1 → [ret] + when { + argDesc.startsWith("L") || argDesc.startsWith("[") -> { + val castTarget = when { + argDesc.startsWith("L") && argDesc.endsWith(";") -> argDesc.substring(1, argDesc.length - 1) + argDesc.startsWith("[") -> argDesc + else -> "java/lang/Object" + } + list.add(TypeInsnNode(Opcodes.CHECKCAST, castTarget)) // [ret-as-T] + list.add(VarInsnNode(Opcodes.ASTORE, slot)) // [] + } + argDesc.isNotEmpty() -> { + addUnbox(list, argDesc) + list.add(VarInsnNode(storeOpcodeOf(argDesc), slot)) + } + else -> { + list.add(InsnNode(Opcodes.POP)) + } + } + list.add(JumpInsnNode(Opcodes.GOTO, end)) + list.add(skip) + list.add(InsnNode(Opcodes.POP)) // 弹掉 dup 残留 → [] + list.add(end) + return list + } + + private fun storeOpcodeOf(desc: String): Int = when (desc) { + "J" -> Opcodes.LSTORE + "D" -> Opcodes.DSTORE + "F" -> Opcodes.FSTORE + "I", "S", "B", "C", "Z" -> Opcodes.ISTORE + else -> Opcodes.ASTORE + } + + // ---- 旧 visitor API(保留兼容,逐步淘汰) -------------------------------- + + /** 发射 dispatcher 调用,栈顶留下 Object 返回值。 */ + fun emitDispatch(mv: MethodVisitor, site: SiteSpec) { + buildDispatch(site).accept(mv) + } + + /** GRAFT:发 dispatcher 并丢弃返回值。 */ + fun emitGraft(mv: MethodVisitor, site: SiteSpec) { + emitDispatch(mv, site) + mv.visitInsn(Opcodes.POP) + } + + /** BYPASS:发 dispatcher 取代原指令;若原指令不产生值则 pop 返回值。 */ + fun emitBypass(mv: MethodVisitor, site: SiteSpec, isVoid: Boolean) { + buildBypass(site, isVoid).accept(mv) + } + + fun buildConditionalBypass(site: SiteSpec, isVoid: Boolean, afterOriginal: LabelNode): InsnList { + val tmp = site.bypassTempSlot + val retDesc = site.bypassReturnDescriptor + if (tmp < 0 || retDesc.isEmpty()) { + return InsnList() + } + val useOriginal = LabelNode() + val list = buildBypassDispatch(site) + list.add(VarInsnNode(Opcodes.ASTORE, tmp)) + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + list.add(MethodInsnNode(Opcodes.INVOKESTATIC, BRIDGE, "isBypassMiss", "(Ljava/lang/Object;)Z", false)) + list.add(JumpInsnNode(Opcodes.IFNE, useOriginal)) + emitPopConsumed(list, site.bypassConsumedDescs) + when { + retDesc == "V" -> Unit + retDesc.startsWith("L") || retDesc.startsWith("[") -> { + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + val castTarget = when { + retDesc.startsWith("L") && retDesc.endsWith(";") -> retDesc.substring(1, retDesc.length - 1) + else -> retDesc + } + if (castTarget != "java/lang/Object") { + list.add(TypeInsnNode(Opcodes.CHECKCAST, castTarget)) + } + } + retDesc.isNotEmpty() -> { + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + addUnbox(list, retDesc) + } + isVoid -> Unit + else -> { + list.add(VarInsnNode(Opcodes.ALOAD, tmp)) + } + } + list.add(JumpInsnNode(Opcodes.GOTO, afterOriginal)) + list.add(useOriginal) + return list + } + + /** TRIM 调度入口 —— 详见 [buildTrim]。 */ + fun emitTrim(mv: MethodVisitor, site: SiteSpec) { + // 直接复用 Tree 形态产物,避免维护两份基本类型 unbox 流水线 + buildTrim(site).accept(mv) + } + + private fun emitTrimReturn(mv: MethodVisitor, site: SiteSpec) { + buildTrimReturn(site).accept(mv) + } + + private fun emitTrimSlot(mv: MethodVisitor, site: SiteSpec) { + buildTrimSlot(site).accept(mv) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/FieldMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/FieldMatcher.kt new file mode 100644 index 000000000..ea9b599a5 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/FieldMatcher.kt @@ -0,0 +1,45 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * FIELD_GET / FIELD_PUT 匹配器 — 对应 [SitePattern.FieldAccess]。 + * + * 接受 anchor 判定由 [anchor] 字段决定;SiteWeaver 创建一个 GET、一个 PUT 实例,避免共享状态。 + */ +class FieldMatcher(sites: List, private val anchor: Anchor) : SiteMatcher { + + init { + require(anchor == Anchor.FIELD_GET || anchor == Anchor.FIELD_PUT) { + "FieldMatcher 仅支持 FIELD_GET / FIELD_PUT,实际=$anchor" + } + } + + override val kind: SiteMatcher.Kind = SiteMatcher.Kind.FIELD + + private val relevant: List = sites.filter { it.anchor == anchor } + private val counters: IntArray = IntArray(relevant.size) + + override fun accepts(site: SiteSpec, pattern: SitePattern): Boolean = + site.anchor == anchor + + fun match(owner: String, name: String, descriptor: String): List { + if (relevant.isEmpty()) return emptyList() + var out: MutableList? = null + for ((i, site) in relevant.withIndex()) { + val ownerOk = site.ownerPattern.isEmpty() || site.ownerPattern == owner + val nameOk = site.namePattern.isEmpty() || site.namePattern == name + val descOk = site.descPattern.isEmpty() || site.descPattern == descriptor + if (!ownerOk || !nameOk || !descOk) continue + val idx = counters[i] + counters[i] = idx + 1 + if (site.ordinal < 0 || site.ordinal == idx) { + if (out == null) out = mutableListOf() + out.add(MatchEvent(site)) + } + } + return out ?: emptyList() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InsnStepMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InsnStepMatcher.kt new file mode 100644 index 000000000..e345738a3 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InsnStepMatcher.kt @@ -0,0 +1,42 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.weaver.site.pattern.InsnStep + +/** + * 单条 [InsnStep] 与一条具体指令视图的比对。 + * + * 规则: + * - `opcode == -1` 表示通配,否则要求精确匹配。 + * - 各 `*Filter` 字段空串视为不过滤;其余按 glob 规则匹配(当前只实现了 `*` 末尾通配与精确相等, + * 后续需要更强的 glob 可在此扩展)。 + * - `cstFilter` 与 `LdcInsn.cst.toString()` 比对。 + */ +object InsnStepMatcher { + + fun matches(step: InsnStep, view: OpcodeSeqMatcher.InsnView): Boolean { + if (step.opcode != -1 && step.opcode != view.opcode) return false + if (!globOk(step.ownerFilter, view.owner)) return false + if (!globOk(step.nameFilter, view.name)) return false + if (!globOk(step.descFilter, view.descriptor)) return false + if (step.cstFilter.isNotEmpty()) { + val cst = view.cst ?: return false + if (!globOk(step.cstFilter, cst.toString())) return false + } + return true + } + + private fun globOk(filter: String, actual: String): Boolean { + if (filter.isEmpty()) return true + if (filter == "*") return true + if (filter.endsWith("*") && !filter.startsWith("*")) { + return actual.startsWith(filter.dropLast(1)) + } + if (filter.startsWith("*") && !filter.endsWith("*")) { + return actual.endsWith(filter.drop(1)) + } + if (filter.startsWith("*") && filter.endsWith("*")) { + return actual.contains(filter.drop(1).dropLast(1)) + } + return filter == actual + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InvokeMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InvokeMatcher.kt new file mode 100644 index 000000000..a3b1952f8 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/InvokeMatcher.kt @@ -0,0 +1,42 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * INVOKE 锚点匹配器 — 对应 [SitePattern.MethodCall]。 + * + * 单条 INVOKE 指令事件:逐 site 比对 owner/name/desc(空串视为通配),推进 ordinal 计数器, + * 当 ordinal 为 -1(全部)或等于当前计数时发出 [MatchEvent]。 + * + * 与原 SiteWeaver.SiteVisitor.matchFor 保持语义一致,便于 streaming / recording 双路径复用。 + */ +class InvokeMatcher(sites: List) : SiteMatcher { + + override val kind: SiteMatcher.Kind = SiteMatcher.Kind.INVOKE + + private val relevant: List = sites.filter { it.anchor == Anchor.INVOKE } + private val counters: IntArray = IntArray(relevant.size) + + override fun accepts(site: SiteSpec, pattern: SitePattern): Boolean = + site.anchor == Anchor.INVOKE + + fun match(owner: String, name: String, descriptor: String): List { + if (relevant.isEmpty()) return emptyList() + var out: MutableList? = null + for ((i, site) in relevant.withIndex()) { + val ownerOk = site.ownerPattern.isEmpty() || site.ownerPattern == owner + val nameOk = site.namePattern.isEmpty() || site.namePattern == name + val descOk = site.descPattern.isEmpty() || site.descPattern == descriptor + if (!ownerOk || !nameOk || !descOk) continue + val idx = counters[i] + counters[i] = idx + 1 + if (site.ordinal < 0 || site.ordinal == idx) { + if (out == null) out = mutableListOf() + out.add(MatchEvent(site)) + } + } + return out ?: emptyList() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/NewMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/NewMatcher.kt new file mode 100644 index 000000000..c0536e5dd --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/NewMatcher.kt @@ -0,0 +1,38 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * NEW 锚点匹配器 — 对应 [SitePattern.TypeAlloc]。 + * + * 单条 NEW 指令事件:比对 type (ASM internal name)。由于 NEW 指令没有 name/desc, + * 只比对 ownerPattern(scanner 在 toSiteSpec 里把 NEW target 放进 ownerPattern)。 + */ +class NewMatcher(sites: List) : SiteMatcher { + + override val kind: SiteMatcher.Kind = SiteMatcher.Kind.NEW + + private val relevant: List = sites.filter { it.anchor == Anchor.NEW } + private val counters: IntArray = IntArray(relevant.size) + + override fun accepts(site: SiteSpec, pattern: SitePattern): Boolean = + site.anchor == Anchor.NEW + + fun match(type: String): List { + if (relevant.isEmpty()) return emptyList() + var out: MutableList? = null + for ((i, site) in relevant.withIndex()) { + val ownerOk = site.ownerPattern.isEmpty() || site.ownerPattern == type + if (!ownerOk) continue + val idx = counters[i] + counters[i] = idx + 1 + if (site.ordinal < 0 || site.ordinal == idx) { + if (out == null) out = mutableListOf() + out.add(MatchEvent(site)) + } + } + return out ?: emptyList() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/OpcodeSeqMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/OpcodeSeqMatcher.kt new file mode 100644 index 000000000..b8d411c56 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/OpcodeSeqMatcher.kt @@ -0,0 +1,106 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.InsnStep +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * OpcodeSeq 匹配器 — 对应 [SitePattern.OpcodeSeq]。 + * + * 使用滑动窗口,在流经的指令序列上尝试匹配 [InsnStep] 列表。当全部步骤按顺序命中后, + * 产出一条 [MatchEvent],其中 `anchorIndex` 指向序列末尾那条指令(录制流索引)。 + * + * 当前只提供骨架:步骤比对下沉到 [InsnStepMatcher];具体的 opcode/owner/name/desc/cst + * 比对在那里实现。真正的窗口推进在 recording 路径由 [taboolib.module.incision.weaver.SiteWeaver] + * 在 Replayer 回放前调用 [match] 完成。 + * + * 注意:streaming 路径不支持 OpcodeSeq(因为需要向前看连续 N 条指令),SiteWeaver 遇到此类 site + * 自动切换到 recording 路径。 + */ +class OpcodeSeqMatcher(private val entries: List) : SiteMatcher { + + override val kind: SiteMatcher.Kind = SiteMatcher.Kind.OPCODE_SEQ + + override fun accepts(site: SiteSpec, pattern: SitePattern): Boolean = + pattern is SitePattern.OpcodeSeq + + /** + * 在已录制的指令流 [events] 上搜索所有命中。 + * + * @param events 描述每条指令关键信息(opcode/owner/name/desc/cst)的序列。索引与录制流一致。 + * @return 所有命中事件,anchorIndex 指向命中序列的最后一条指令在录制流中的索引。 + */ + fun match(events: List): List { + if (entries.isEmpty() || events.isEmpty()) return emptyList() + val out = ArrayList() + for (entry in entries) { + val seq = entry.steps + if (seq.isEmpty()) continue + // 滑窗:在 events 上尝试从每个位置起匹配整个序列 + var matchedOrdinal = 0 + var i = 0 + while (i < events.size) { + val end = tryMatchAt(events, i, seq) + if (end >= 0) { + if (entry.site.ordinal < 0 || entry.site.ordinal == matchedOrdinal) { + out += MatchEvent(entry.site, anchorIndex = end) + } + matchedOrdinal++ + i = end + 1 + } else { + i++ + } + } + } + return out + } + + /** + * 从 events[start] 开始尝试匹配整个 seq;成功返回命中末尾索引,失败返回 -1。 + * + * 语义是“以 start 作为首 step 的锚点,后续 step 允许向前跳过不相关指令”, + * 而不是“所有 step 必须严格相邻”。 + * + * 这样才能覆盖: + * - `LDC;LDC;INVOKEVIRTUAL` 之间夹着 ASTORE / NEW / DUP / ALOAD + * - `PUTFIELD repeat=2/5`、`ARRAYLENGTH repeat=2` 之间夹着装载与常量指令 + */ + private fun tryMatchAt(events: List, start: Int, seq: List): Int { + if (start >= events.size || !InsnStepMatcher.matches(seq.first(), events[start])) return -1 + var cursor = start + 1 + var end = start + for ((stepIndex, step) in seq.withIndex()) { + val repeat = step.repeat.coerceAtLeast(1) + repeat(repeat) { repeatIndex -> + if (stepIndex == 0 && repeatIndex == 0) { + end = start + return@repeat + } + var matchedAt = -1 + while (cursor < events.size) { + if (InsnStepMatcher.matches(step, events[cursor])) { + matchedAt = cursor + cursor++ + break + } + cursor++ + } + if (matchedAt < 0) return -1 + end = matchedAt + } + } + return end + } + + /** 录制流里单条指令的"视图"——给匹配器使用的最小可比对信息。 */ + data class InsnView( + val opcode: Int, + val owner: String = "", + val name: String = "", + val descriptor: String = "", + val cst: Any? = null, + ) + + /** 组合 OpcodeSeq site 与其 steps。SiteWeaver 构建时从 site 对应的 SitePattern.OpcodeSeq 拆出 steps。 */ + data class Entry(val site: SiteSpec, val steps: List) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/SiteMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/SiteMatcher.kt new file mode 100644 index 000000000..a260ccd33 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/SiteMatcher.kt @@ -0,0 +1,36 @@ +package taboolib.module.incision.weaver.site.matcher + +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * 单条指令事件传入 matcher 后产出的匹配结果。 + * + * @property siteSpec 命中的 site(保留 kind/shift/ordinal/target 供 planner/emitter 使用)。 + * @property anchorIndex 锚点指令在录制流中的序号(仅 recording 路径可用,streaming 路径留 -1)。 + * OpcodeSeq 类型下指向序列末尾那条指令。 + */ +data class MatchEvent( + val siteSpec: SiteSpec, + val anchorIndex: Int = -1, +) + +/** + * 指令匹配器基础接口。按事件类型提供 `matchInvoke` / `matchField` / `matchType` / `matchTerminal` + * 会导致接口臃肿;这里采用"上下文对象"思路,由 [taboolib.module.incision.weaver.SiteWeaver] 分类后调用对应 matcher。 + * + * 每个实现对应一种 [SitePattern] 分支,使用滑窗时额外在 matcher 内部维护状态。 + */ +interface SiteMatcher { + + /** 该 matcher 对应的 pattern 标签(用于 SiteWeaver 按 site 类型分派)。 */ + val kind: Kind + + /** + * 判断给定 site 是否被该 matcher 负责。 + * 用于 SiteWeaver 初始化时把 sites 分桶给各 matcher。 + */ + fun accepts(site: SiteSpec, pattern: SitePattern): Boolean + + enum class Kind { INVOKE, FIELD, NEW, TERMINAL, OPCODE_SEQ } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/TerminalMatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/TerminalMatcher.kt new file mode 100644 index 000000000..8a8b7f955 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/matcher/TerminalMatcher.kt @@ -0,0 +1,55 @@ +package taboolib.module.incision.weaver.site.matcher + +import org.objectweb.asm.Opcodes +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * 终结性锚点匹配器 — 对应 [SitePattern.Anywhere]:RETURN / THROW / HEAD / TAIL。 + * + * HEAD 对应方法入口(由 SiteWeaver 在 visitCode 后发),TAIL 暂时与 RETURN 同语义(每个 return 前)。 + * THROW 对应 ATHROW 指令,RETURN 对应 IRETURN..RETURN 范围。 + */ +class TerminalMatcher(sites: List) : SiteMatcher { + + override val kind: SiteMatcher.Kind = SiteMatcher.Kind.TERMINAL + + private val returnSites: List = sites.filter { + it.anchor == Anchor.RETURN || it.anchor == Anchor.TAIL + } + private val throwSites: List = sites.filter { it.anchor == Anchor.THROW } + private val headSites: List = sites.filter { it.anchor == Anchor.HEAD } + + private val returnCounters: IntArray = IntArray(returnSites.size) + private val throwCounters: IntArray = IntArray(throwSites.size) + private val headCounters: IntArray = IntArray(headSites.size) + + override fun accepts(site: SiteSpec, pattern: SitePattern): Boolean = + site.anchor == Anchor.RETURN || site.anchor == Anchor.TAIL || + site.anchor == Anchor.THROW || site.anchor == Anchor.HEAD + + /** 指令 opcode 为 ATHROW 或 IRETURN..RETURN 时调用。 */ + fun matchInsn(opcode: Int): List = when { + opcode == Opcodes.ATHROW -> collect(throwSites, throwCounters) + opcode in Opcodes.IRETURN..Opcodes.RETURN -> collect(returnSites, returnCounters) + else -> emptyList() + } + + /** SiteWeaver 在 visitCode 之后调用一次,发出 HEAD 事件。 */ + fun matchHead(): List = collect(headSites, headCounters) + + private fun collect(sites: List, counters: IntArray): List { + if (sites.isEmpty()) return emptyList() + var out: MutableList? = null + for ((i, site) in sites.withIndex()) { + val idx = counters[i] + counters[i] = idx + 1 + if (site.ordinal < 0 || site.ordinal == idx) { + if (out == null) out = mutableListOf() + out.add(MatchEvent(site)) + } + } + return out ?: emptyList() + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/InsnStep.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/InsnStep.kt new file mode 100644 index 000000000..d7ec53977 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/InsnStep.kt @@ -0,0 +1,23 @@ +package taboolib.module.incision.weaver.site.pattern + +/** + * 单条字节码指令的过滤步骤 — 模型层表示,等价于注解 [taboolib.module.incision.annotation.Step] + * 的展平形式。 + * + * 字段中的 `*Filter` 均为 glob 风格字符串:空串表示"不过滤",`*` 表示通配。 + * + * @property opcode 期望的 ASM 指令值;`-1` 表示通配。 + * @property ownerFilter 所属类(ASM internal name 或 glob)。 + * @property nameFilter 方法名 / 字段名(支持 glob)。 + * @property descFilter 方法描述符或字段类型描述符(支持 glob)。 + * @property cstFilter 常量值匹配(适用于 LDC / BIPUSH / SIPUSH 等)。 + * @property repeat 该步骤连续匹配的次数。 + */ +data class InsnStep( + val opcode: Int, + val ownerFilter: String = "", + val nameFilter: String = "", + val descFilter: String = "", + val cstFilter: String = "", + val repeat: Int = 1, +) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SiteOffset.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SiteOffset.kt new file mode 100644 index 000000000..91f57ee12 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SiteOffset.kt @@ -0,0 +1,24 @@ +package taboolib.module.incision.weaver.site.pattern + +import taboolib.module.incision.api.Shift + +/** + * 锚点偏移 — 沿 [shift] 方向跨越 [units] 条字节码指令。 + * + * `units = 0` 表示就在锚点处,不发生位移。 + * + * @property shift 偏移方向([Shift.BEFORE] / [Shift.AFTER])。 + * @property units 跨越的指令条数。 + */ +data class SiteOffset( + val shift: Shift, + val units: Int, +) { + + companion object { + + /** 锚点原位(不偏移)。 */ + @JvmField + val NONE: SiteOffset = SiteOffset(Shift.BEFORE, 0) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SitePattern.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SitePattern.kt new file mode 100644 index 000000000..1115c0ba4 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/SitePattern.kt @@ -0,0 +1,58 @@ +package taboolib.module.incision.weaver.site.pattern + +import taboolib.module.incision.api.Anchor + +/** + * Site 模式 —— 描述如何在目标方法字节码中定位匹配点的模型层 sealed class。 + * + * 由 weaver-eng 的解析器(`weaver/site/parser/`)产出,被 weaver 消费用于真实的指令匹配。 + * + * 各分支代表不同的匹配策略: + * - [MethodCall] : INVOKEVIRTUAL / INVOKESPECIAL / INVOKESTATIC / INVOKEINTERFACE 调用点 + * - [FieldAccess]: GETFIELD / PUTFIELD / GETSTATIC / PUTSTATIC 字段访问点 + * - [TypeAlloc] : NEW 指令(对象构造) + * - [Anywhere] : 方法语义级锚点(HEAD/TAIL/RETURN/THROW),不依赖指令内容 + * - [OpcodeSeq] : 由 [InsnStep] 组成的连续指令序列 + */ +sealed class SitePattern { + + /** + * 方法调用匹配 — 各字段支持 glob 通配,空串表示不过滤。 + */ + data class MethodCall( + val owner: String, + val name: String, + val desc: String, + ) : SitePattern() + + /** + * 字段访问匹配 — 各字段支持 glob 通配,空串表示不过滤。 + */ + data class FieldAccess( + val owner: String, + val name: String, + val desc: String, + ) : SitePattern() + + /** + * 对象构造匹配 — [internalName] 为被构造类的 ASM internal name(如 `java/util/ArrayList`)。 + */ + data class TypeAlloc( + val internalName: String, + ) : SitePattern() + + /** + * 方法语义锚点 — 仅依赖锚点类型,不需匹配具体指令内容。 + * 仅接受 [Anchor.HEAD] / [Anchor.TAIL] / [Anchor.RETURN] / [Anchor.THROW]。 + */ + data class Anywhere( + val anchor: Anchor, + ) : SitePattern() + + /** + * 指令序列匹配 — 按 [steps] 顺序匹配若干条连续字节码指令。 + */ + data class OpcodeSeq( + val steps: List, + ) : SitePattern() +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/FieldTargetParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/FieldTargetParser.kt new file mode 100644 index 000000000..760666503 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/FieldTargetParser.kt @@ -0,0 +1,44 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.DescriptorCodec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * FIELD_GET / FIELD_PUT 锚点解析器 — 把 `owner#name:type` 解析为 [SitePattern.FieldAccess]。 + * + * 兼容规则: + * - raw 为空:owner/name/desc 全空。 + * - 带冒号:完整 `owner#name:Type` —— 走 [DescriptorCodec.parseField]。 + * - 不带冒号:降级只按 `owner#name` 匹配,desc 留空。 + * - 连 `#` 都没有:降级为只过滤 name。 + * + * [anchor] 允许外部传入以区分 GET/PUT,默认 FIELD_GET(两者的解析逻辑完全相同, + * 仅用于 [ParserRegistry] 的键区分)。 + */ +class FieldTargetParser(override val anchor: Anchor = Anchor.FIELD_GET) : SiteTargetParser { + + init { + require(anchor == Anchor.FIELD_GET || anchor == Anchor.FIELD_PUT) { + "FieldTargetParser 只接受 FIELD_GET / FIELD_PUT,实际=$anchor" + } + } + + override fun parse(raw: String): SitePattern? { + if (raw.isBlank()) return SitePattern.FieldAccess("", "", "") + val parsed = DescriptorCodec.parseField(raw) + if (parsed != null) { + return SitePattern.FieldAccess(parsed.owner, parsed.name, parsed.descriptor) + } + val hash = raw.indexOf('#') + return if (hash >= 0) { + SitePattern.FieldAccess( + owner = raw.substring(0, hash).replace('.', '/'), + name = raw.substring(hash + 1), + desc = "", + ) + } else { + SitePattern.FieldAccess(owner = "", name = raw, desc = "") + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/InvokeTargetParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/InvokeTargetParser.kt new file mode 100644 index 000000000..7a6938d9f --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/InvokeTargetParser.kt @@ -0,0 +1,25 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.api.DescriptorCodec +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * INVOKE 锚点解析器 — 把 `owner#name(args)ret` 解析为 [SitePattern.MethodCall]。 + * + * 兼容规则(与既有 SurgeonScanner.toSiteSpec 一致,便于平滑迁移): + * - raw 为空:owner/name/desc 全空,表示不过滤(匹配所有 INVOKE)。 + * - raw 不含 `(`:只写方法名 `owner#name`,`()V` 的默认描述符按"不过滤"处理(desc 留空)。 + * - [DescriptorCodec] 解析失败返回 null。 + */ +class InvokeTargetParser : SiteTargetParser { + + override val anchor: Anchor = Anchor.INVOKE + + override fun parse(raw: String): SitePattern? { + if (raw.isBlank()) return SitePattern.MethodCall("", "", "") + val parsed = DescriptorCodec.parseMethod(raw) ?: return null + val desc = if (parsed.descriptor == "()V" && '(' !in raw) "" else parsed.descriptor + return SitePattern.MethodCall(parsed.owner, parsed.name, desc) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NewTargetParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NewTargetParser.kt new file mode 100644 index 000000000..40e27bc9e --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NewTargetParser.kt @@ -0,0 +1,20 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * NEW 锚点解析器 — 把 `java.util.ArrayList` 或 `java/util/ArrayList` 解析为 + * [SitePattern.TypeAlloc]。 + * + * 兼容规则:raw 为空时 internalName 为空串,表示不过滤(匹配所有 NEW)。 + */ +class NewTargetParser : SiteTargetParser { + + override val anchor: Anchor = Anchor.NEW + + override fun parse(raw: String): SitePattern { + if (raw.isBlank()) return SitePattern.TypeAlloc("") + return SitePattern.TypeAlloc(raw.replace('.', '/')) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NoTargetParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NoTargetParser.kt new file mode 100644 index 000000000..40f19c219 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/NoTargetParser.kt @@ -0,0 +1,23 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * 无 target 锚点解析器 — 用于 [Anchor.HEAD] / [Anchor.TAIL] / [Anchor.RETURN] / [Anchor.THROW]。 + * + * 这些锚点不依赖具体指令内容,直接产出 [SitePattern.Anywhere]。 + * 非空 raw 视为"仅供文档说明",不影响解析结果。 + * + * [anchor] 由构造器传入以便 [ParserRegistry] 区分四个注册项。 + */ +class NoTargetParser(override val anchor: Anchor) : SiteTargetParser { + + init { + require(anchor == Anchor.HEAD || anchor == Anchor.TAIL || anchor == Anchor.RETURN || anchor == Anchor.THROW) { + "NoTargetParser 只接受 HEAD / TAIL / RETURN / THROW,实际=$anchor" + } + } + + override fun parse(raw: String): SitePattern = SitePattern.Anywhere(anchor) +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/ParserRegistry.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/ParserRegistry.kt new file mode 100644 index 000000000..f46488a9e --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/ParserRegistry.kt @@ -0,0 +1,57 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * Site target parser 注册中心 —— 按 [Anchor] 分派到对应 [SiteTargetParser]。 + * + * 默认注册所有内置 parser: + * - INVOKE → [InvokeTargetParser] + * - FIELD_GET → [FieldTargetParser] (FIELD_GET) + * - FIELD_PUT → [FieldTargetParser] (FIELD_PUT) + * - NEW → [NewTargetParser] + * - HEAD/TAIL/RETURN/THROW → [NoTargetParser] + * + * 使用 `@JvmField` 的 [DEFAULT] 实例即可满足 SurgeonScanner 的调用需求; + * 如需自定义扩展(例如新加 InsnPattern 走字面量识别的解析器),可 new 出新 Registry + * 并用 [register] 覆盖/新增。 + */ +class ParserRegistry private constructor( + private val parsers: Map, +) { + + /** + * 根据锚点解析 raw target,产出 [SitePattern]。 + * 若对应锚点未注册 parser,返回 `null`;parser 本身解析失败也返回 `null`。 + */ + fun parse(anchor: Anchor, raw: String): SitePattern? { + val parser = parsers[anchor] ?: return null + return parser.parse(raw) + } + + /** 获取指定锚点的 parser(未注册返回 null)。用于单测或高级定制。 */ + fun parserOf(anchor: Anchor): SiteTargetParser? = parsers[anchor] + + /** 以当前 registry 为基础派生一份新 registry,覆盖或新增 parser。 */ + fun register(parser: SiteTargetParser): ParserRegistry = + ParserRegistry(parsers + (parser.anchor to parser)) + + companion object { + + /** 默认注册表 —— 覆盖所有 [Anchor] 枚举值。 */ + @JvmField + val DEFAULT: ParserRegistry = ParserRegistry( + mapOf( + Anchor.INVOKE to InvokeTargetParser(), + Anchor.FIELD_GET to FieldTargetParser(Anchor.FIELD_GET), + Anchor.FIELD_PUT to FieldTargetParser(Anchor.FIELD_PUT), + Anchor.NEW to NewTargetParser(), + Anchor.HEAD to NoTargetParser(Anchor.HEAD), + Anchor.TAIL to NoTargetParser(Anchor.TAIL), + Anchor.RETURN to NoTargetParser(Anchor.RETURN), + Anchor.THROW to NoTargetParser(Anchor.THROW), + ) + ) + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/SiteTargetParser.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/SiteTargetParser.kt new file mode 100644 index 000000000..0b2340645 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/pattern/parser/SiteTargetParser.kt @@ -0,0 +1,23 @@ +package taboolib.module.incision.weaver.site.pattern.parser + +import taboolib.module.incision.api.Anchor +import taboolib.module.incision.weaver.site.pattern.SitePattern + +/** + * Site target 解析策略 — 把用户写在 `@Site.target` 上的原始字符串解析为 [SitePattern]。 + * + * 每个 [Anchor] 对应一个解析器实现;解析失败返回 `null`(调用方再决定降级或忽略)。 + * target 为空字符串视为 "不过滤",这种情况也走对应的 parser。 + */ +interface SiteTargetParser { + + /** 该 parser 适用的锚点类型(用于 [ParserRegistry] 查表)。 */ + val anchor: Anchor + + /** + * 解析原始 target。 + * @param raw 用户写的原始字符串,可能为空。 + * @return 解析出的 [SitePattern],失败返回 `null`。 + */ + fun parse(raw: String): SitePattern? +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/EmissionPlan.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/EmissionPlan.kt new file mode 100644 index 000000000..e3dfa050c --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/EmissionPlan.kt @@ -0,0 +1,23 @@ +package taboolib.module.incision.weaver.site.planner + +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.matcher.MatchEvent + +/** + * 发射计划 —— 由 planner 根据 [MatchEvent] 结合 site 特性(offset / pattern 种类)产出, + * emitter 据此决定何时把 dispatcher 调用写入字节码流。 + * + * 三种策略: + * - [Strategy.IMMEDIATE]:锚点原位(offset == 0,非 OpcodeSeq),可以在 streaming 路径直接发射; + * - [Strategy.DEFERRED]:AFTER + offset > 0,需要在流过若干条后续指令后触发; + * - [Strategy.BUFFERING]:BEFORE + offset > 0(或任何需要向前看的场景),必须走 recording 路径。 + */ +data class EmissionPlan( + val event: MatchEvent, + val strategy: Strategy, +) { + + val site: SiteSpec get() = event.siteSpec + + enum class Strategy { IMMEDIATE, DEFERRED, BUFFERING } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/PlannerDispatcher.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/PlannerDispatcher.kt new file mode 100644 index 000000000..82cf7d901 --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/planner/PlannerDispatcher.kt @@ -0,0 +1,57 @@ +package taboolib.module.incision.weaver.site.planner + +import taboolib.module.incision.api.Shift +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.matcher.MatchEvent + +/** + * 发射策略规划器 —— 根据 site 的 anchor / shift / offset 把 [MatchEvent] 归为三种 [EmissionPlan.Strategy]。 + * + * 策略矩阵(offset 的绝对值用于 skipForward/skipBackward 计数): + * - `anchorIndex >= 0`(OpcodeSeq 命中走 recording 路径)→ BUFFERING,保持语义:插入在序列末指令之前 + * - `offset > 0 && shift == AFTER` → DEFERRED,锚点 + offset 条真实指令后 + * - `offset > 0 && shift == BEFORE` → BUFFERING,锚点 - offset 条真实指令前(反向跳跃) + * - `offset < 0` → BUFFERING,锚点 - |offset| 条真实指令前(shift 方向不再影响) + * - HEAD 锚点(由 SiteWeaver 单独走 headEvents 流程插入)→ 这里不命中,但保留 IMMEDIATE 兜底 + * - 其余 `offset == 0` → IMMEDIATE,由 [SiteWeaver.applyPlan.insertAtAnchor] 按 shift/kind 就地插入 + */ +class PlannerDispatcher { + + fun plan(event: MatchEvent): EmissionPlan { + val spec = event.siteSpec + val offset = spec.offset + return when { + // 先按 offset 路由:非零 offset 的语义覆盖任何 anchorIndex/pattern 出处 + offset > 0 && spec.shift == Shift.AFTER -> EmissionPlan(event, EmissionPlan.Strategy.DEFERRED) + offset != 0 -> EmissionPlan(event, EmissionPlan.Strategy.BUFFERING) + // OpcodeSeq 命中(pattern 为 OpcodeSeq 且 anchorIndex 指向序列末)→ BUFFERING:插入在序列末前 + event.anchorIndex >= 0 && spec.pattern is taboolib.module.incision.weaver.site.pattern.SitePattern.OpcodeSeq -> + EmissionPlan(event, EmissionPlan.Strategy.BUFFERING) + // 其余 offset == 0 场景 → IMMEDIATE,由 SiteWeaver 按 shift/kind 就地插入 + else -> EmissionPlan(event, EmissionPlan.Strategy.IMMEDIATE) + } + } + + /** 批量规划便捷入口,保留顺序。 */ + fun planAll(events: List): List = events.map(::plan) + + companion object { + + /** + * 判断整组 sites 是否允许全走 streaming 路径(无需 recording 开销)。 + * 条件:没有 OpcodeSeq 类型的 site(pattern 判定由调用方事先完成)。 + */ + fun canStream(sites: List, hasOpcodeSeq: Boolean): Boolean { + if (sites.isEmpty()) return true + if (hasOpcodeSeq) return false + // BYPASS 需要稳定且不与宿主局部变量冲突的 tmp slot 来保存 dispatch 返回的 Object。 + // streaming 路径在 visitMethodInsn 时就要 ASTORE,但此时还没看到宿主方法所有 VarInsn, + // 拿不到真实 maxLocals——只能按 args 估算,会撞上方法体内声明的局部(曾观察到 + // invokeHelper(I)I 的 tmp 落在 slot 2 == 局部 a,导致 IRETURN 时 Expected I, but found R)。 + // 走 recording 路径时 TreeMethodDriver 已在 visitEnd 拿到完整 MethodNode.maxLocals, + // resolveBypassMeta 会安全地把 tmpSlot 分配在所有局部之上。 + if (sites.any { it.kind == taboolib.module.incision.runtime.AdviceKind.BYPASS }) return false + return true + } + } +} diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/InsnAction.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/InsnAction.kt new file mode 100644 index 000000000..e5546e86c --- /dev/null +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/site/recorder/InsnAction.kt @@ -0,0 +1,204 @@ +package taboolib.module.incision.weaver.site.recorder + +import org.objectweb.asm.Handle +import org.objectweb.asm.Label +import org.objectweb.asm.TypePath +import taboolib.module.incision.runtime.AdviceKind +import taboolib.module.incision.weaver.site.SiteSpec +import taboolib.module.incision.weaver.site.emitter.DispatcherEmitter + +/** + * 录制下来的单条 ASM `MethodVisitor` 调用。 + * + * 设计目标:可在 [Replayer] 中按序回放到任意 `MethodVisitor`,且支持在录制流中间插入额外动作 + * (由 BufferingPlanner 在 BEFORE+offset 等场景下使用)。 + * + * 注意:这里只覆盖 SiteWeaver 当前涉及的 visit* 方法 + 必需的元数据(Frame / TryCatch / + * LocalVariable / LineNumber / Label / Maxs / End),不覆盖注解类的 visit*Annotation* —— 它们由 + * Scalpel 主 pass 直接走 ClassVisitor 处理。 + */ +sealed class InsnAction { + + abstract fun replay(mv: org.objectweb.asm.MethodVisitor) + + data class Insn(val opcode: Int) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitInsn(opcode) + } + + data class IntInsn(val opcode: Int, val operand: Int) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitIntInsn(opcode, operand) + } + + data class VarInsn(val opcode: Int, val variable: Int) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitVarInsn(opcode, variable) + } + + data class TypeInsn(val opcode: Int, val type: String) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitTypeInsn(opcode, type) + } + + data class FieldInsn(val opcode: Int, val owner: String, val name: String, val descriptor: String) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitFieldInsn(opcode, owner, name, descriptor) + } + + data class MethodInsn( + val opcode: Int, + val owner: String, + val name: String, + val descriptor: String, + val isInterface: Boolean, + ) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = + mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface) + } + + data class InvokeDynamicInsn( + val name: String, + val descriptor: String, + val handle: Handle, + val args: Array, + ) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = + mv.visitInvokeDynamicInsn(name, descriptor, handle, *args) + } + + data class JumpInsn(val opcode: Int, val label: Label) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitJumpInsn(opcode, label) + } + + data class LabelMark(val label: Label) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitLabel(label) + } + + data class LdcInsn(val cst: Any) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitLdcInsn(cst) + } + + data class IincInsn(val variable: Int, val increment: Int) : InsnAction() { + override fun replay(mv: org.objectweb.asm.MethodVisitor) = mv.visitIincInsn(variable, increment) + } + + data class TableSwitchInsn( + val min: Int, + val max: Int, + val dflt: Label, + val labels: Array

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 C;ptHT literal 0 HcmV?d00001 diff --git a/module/incision/src/main/resources/native/macos/arm64/libincision-jvmti.dylib b/module/incision/src/main/resources/native/macos/arm64/libincision-jvmti.dylib new file mode 100644 index 0000000000000000000000000000000000000000..f544dcb1aded7f8d8a2b1ed7a99bf6b3eb34a7e6 GIT binary patch literal 69272 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 literal 0 HcmV?d00001 diff --git a/module/incision/src/main/resources/native/macos/x64/libincision-jvmti.dylib b/module/incision/src/main/resources/native/macos/x64/libincision-jvmti.dylib new file mode 100644 index 0000000000000000000000000000000000000000..40ecb6fd339fba8493bcefccbb80ffcf5b803604 GIT binary patch literal 27796 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 literal 0 HcmV?d00001 diff --git a/module/incision/src/main/resources/native/windows/arm64/incision-jvmti.dll b/module/incision/src/main/resources/native/windows/arm64/incision-jvmti.dll new file mode 100644 index 0000000000000000000000000000000000000000..cbb409ae997a383f08e636e7923866ab6ade64b1 GIT binary patch literal 182784 zcmeFa4P2Dhl|O!;8IWgYKzZ{G4Ko8;X9lA&QG>fqc>pyCc@s&yN!ta)v>=)UleFSX z0nr2L-x7xJf(X|iHo5k4u z%Nbki4q`z+`!|h({tAmR88j0aOGVLEsWdbZ|3k&^#YCt09n}-WqQU70_nQn8V_EYyuFZZTo3Zj7z+?etE&kt&|EfQM zF=5`CC-c^?&09-gL@OhZH%5Sy(6Ou})VIlSM~A`H&wES)6nO!jh&29c7@RQgiS>_D zmm=2%z!Avrhr!j)+xYPMM?sv(OYl^LYa9lbM7aL{zrQ{Q`n6ok)zzHq+IKWA-xjUQ zcak}EOz`G%?DYm#oX*~6-u77LjA2z9Ez<>We-QIBF5$xtEwi-+30@<|3IcfHh7iOu zc(gO4ow@3wwV@R~r&XN(;s zIz)J9vWU|-lWi0O>cK=&aXfen>dlLDM;{ zQKIQpiSDc~`$f9vO=SB%2W`-A=Nc#52ZMI>^B{Kg;~3V{fi}q#ScZ;GZcf<7dO8DH z#>d-PPZGy6sBR`>k%RcXySD7r3C!tEV$N=iJ!wx-O@0aM zNiAW{W#y(v9>ynT<|~iH&V2Qem^}$KR+P7Y*`M*ThVL%cqR$E2JkZGu;F|<|lL$wn zXWL#qGdS`%zWI>G;8?R$=(C#Hk>qmIyqK8>=OyiAecAmI4p`odCJo=LXS}0Y!#lbI zi_?QA3-Oh8tYAG?9%T(=J$FxL@dQtEWz1b1Pdt(MrkXR1jpj@m^Aw4nvP_9Gpq=8L z2p&>B&F2H2{-gZ&lD%t4#u&zg`9H})eq|i~b3W4CN`1+^h0G{zi8itQkTS?E$(>xb z8FSE|JOR2i8FDej)%P#V>-%D=%XdD|`9sh|I?&iEcsn%gXs(9jG6G}PxcLX9ex3kK z@hRpt@lt&|;6;6TX`-Yr|1r@?dP41pF49kSntAJ?GbYI79gq(yyGA(Q7QEFQJ4XEw z-hD9>oKk-n#|BC6Mg7$pZPfv6Q5oQJpCp%ok`ypCzXxKJX99 zyheQ`c;Nfu+u)zzz4o1NLBAewfv=)|u}|2e_26Ou%gn38>Sk?OfO%$4%^c8L%4*t5 zIeawuXz>ZaClH?r_ypm@*R*Mi?KEed5uoAElD!~&X7I0JLUHf`u7?6;Qr!<8p7YCgDTT2EISZc}^Q)3kYHRsE*3>pJV%qG_0U)YGssf zi)@2VGLc6j&FBkuxu-3tGK%cd)z!a+OmeXA5=}J6B#&hnI}aPA*vWSAU*UrlJfQi0 z60rL*PbXkIY0gNFl=(W2xhe(@>eC2WWLkFgaeUAIQ1I@T$I6#T{2@HYl7gI5gyQr- z%>RcISkG}it2?%aIZXj9qiTC*Zx;HPje({By)eg@tW?#F>6QpT|l zIEsMdao`YOcd}$0Jn;uOvSb_q62C+oWgL6X*u>Ql->!Wp5%P7ins`LxAi8OOG&=?F zNN@r(f|vgy^749)#^s$svWhkHytI~Ghwim&wW9t1TpQs24BvUI0mN(4SrcqM@ldi~ zr2nvMHIfWaxtG>d(JoAp){yqWIsy9G3q1T}#db^v%v7v@)7+nEr`rfW;kXW%K^)WE z$&eYIlXPC0KgHJa^7^5a^Qtwm3j7=>UoOodeC|kUqu3IXdFmH>4KEk#6X@}Mkqo|o zjvR%*Z;SlxKRM`UT+h+Ey!ipHFCGtsOiqBFaLz)^t+@6>1}$d=Zwhu5WH%czCmkW| zXc@+K75r_3JtDhFYizH~U!tKtSb~vwOEA+0cnhA`+I}oNzw=qPkMNM3-XLC@lQ9+v z7Wi!EwNq`MXrrXL0u3i*c~E4yS|cpE0bTBZuDT{LXKmH?bp6z6hJWCAg9U3`b$|aN z*d>`qgqM7j0Up`xsG~MBA-|h3wufS&4_eGmEDN8J_p6t)w1PJa`bcZnlc2R8HuI-J ztYBIY3!^pq2G+V>&`ErW68SyZ{S@Hncb1lUv!HV%w*>d22v($ zmujq&Bgq8m8_7czbd6+z>~{MI^T>}eha}51Pt!3^ThQMY%+pMHo}xCq^|F9@A{nB2 zGGLrlj4fQEnx`(=9bEnQBLdoGVq=1N%r`}WXvrm${zKPc^*pp z_`G-fwk!w#mVwvF;I*QEznAncd8q#Rwt&y@TgfhyefkXjRe@LJuaj($?3?jjT(2M# ziVo1)VS%h{!vC^2xH_5-TF1J&{4K~Mw&X|Ku%3`iejhv`ncM=Igj|N5 zfb9r@Oi_Iz>IBGLCfN(pfBeqG{F2?slx+rV#VWEDZ5T%$`ZL1^62~%%3~I3^j7EDF z8+py_n8SR^y4fr8GeETISG9reYgjvpr(W21;xElN>5a6uDRvcmELqoS|8O0C@Q3&i zoMfA(10SuASYr$2{UzkN3G+ZO=g=m>%oW^?Jy${qR}ieje;rp)A1eJmVZwfP7x~-L z`k>H&HP|NlWh@*%HFSIWpiP9~lzjHyP|0V%KV0_N3CHw-aZumd1Qt$v*7q?l*D()+ zbW7{HVpGWu(E9q5D0b{4*{+EFE|>bH`hP=t!b+}SF81IFv$=w*5d61t1^c5gh8IR0 zgIgTKg^1C{@LZBKhUaoDk_kkoGB*m}iAP%Y{eLF@BR&_Ia1Z8)#+QfjMSzZy;dIDz^ql$i z(DBDNElfp+-y9jGIm$=B{LV4@c_M85eqJBBAFYu^tRno*G4Q9{#5_G0G3q!ZUQQ=p z(MJ4SLjC=0cz@#j1y2^oG7UUe&z~&YgY$R)D6F4`7p2%vM&su#oaMS@Qvoj^4o@v;Zy8OhU^L8!zgPj z*-W)?f9Xuin4%~kB19y$a2#=`I)_AdJt z=nia`IOno0Q|ORm56>kqYZAR@FxO+*v?(*# zFe#&d(_r6<%@h0mt+Zx6O?|`W zWl`T`^CqeKmhIcHzN;ro^H!@FyYCkU`{rLwB!3`sU|h0I8;1KoW!wSd@N~Zlw=2=R zQjJ@-XTxxp$vite4(_*AxQ``zZ&%}%?bk5ew@sCJk~PkJbPnPkG9M?T`G{5bCEKiF zecdgOW79a}s2l8yKa%K$?Ag?P65b%{Z~vgpA=x-D?@<+9A)k+Y9mR$XmP@ux^3R6r zTNb-n-%N^WT!qcLChxib=YARKmF$-#O8X+ySM|$QQ4C^4zw9~l=ziJYI`Y54FGK8a zgfWm`_MG`I@yoJ@!VqagyCo4=E-6zW%v^QV3Pu8ET!5N|kad9o;xB=KF1tPAC zbCNAVu8w5H%@8}Mz2z6GxLF_emY>UeOD}90#dPU>gnY={f!LXJ<|O7PU=K=l6tDad z&Xyj)hv15|tVd@qPS+RE`7gmwbFbufRY*P?&c^fddZ``G?8ID+w-6Jhb`tE&fio9l z04r!l9zpLUhCMK|aX5>oz4cN2zGWBUlZaQ+8I1Rj%;iN4>sL5Wxf8xF?HdR2NijUd zul)e$u>YvR*#zLdfK}j>y`u7)Vr^=Fc(9z#)i4Hmub&27N}VF7wEt0fMteCL*GRIe zz)1UG;uB(l^4>w%vZN2^n@+@DoeA1$-C5IFMkC^m##dPZ!*`|6cI`~iGu_4?ljee8 z#CWofTV;Hh735;iXMpbIo0+reMt|tKyxe%JPVX>rMsZGwHj!@lIiu2z{hBEE@_tRB zo5%j_pH(~*@xbpK6%Y2(A|81!t>76let92_@%PpvE~1Q6#0|fERNRO&h`8lAgMypl z3Nt`s2JjJY8Vzir@ggfIHn0rhFO`+y%4n-$Z=uL%DGQes}d}RO8%$c#K#;Mq{*erb)j)obr>3yZz~dA3rL6h(UX*>jSd+duJr_2I z;#+^jSy2e`2Ppo1E5*1FD-jT%p;!rv#Qz7mf<*l1eTt2m;ij{Y8ss`rn~M}rLYpeY z%BfA2S!#2_iZ*t#@sbUca?PZ(VT#9-o<1~-9gF1MM?||ww-_G--Kz5dd0azjA2<(?Y$?%B=K%z(mKWtbpj)Oji}(my6K8^!>7&y^ z^Ci+F?ybk+BhEZl06*^<9Q!o--81-m9{oNs_&bGupC0@z#?pQ^_FdAXhn(UobjfM4pKLS%f=M74s*)50@_`=0CKb!TFhn*c8@5 z#Ntb-UvBh%2jp7x*Txy&XG+|EXupu@fw=$QQGX#f(Vyt&j?>@3S<2A<{KhGsr9@M| zf4Yfz6k{Ia^gD1CHN0QUrFdSb*a^}Pl4-~l&V8fY%}=p?G-p!%&G|nHygKiPVp8?6 z{R4G!9G+}mM>5Vuw%i3@bOn6UFSBb=S2)@M|a7%8(0l7CAgLzUMJkf?~FZ!9}n-%-|j}lL491@SG z*p!?FDrY#GAzzyEIHY=t$x=?y6yy|59q_|fqR;K<)7&J@73FV{zI+0{{~7C@VzcQ? zoA9XTM!=foKQGE}(f|D3pzo^Cpg|d~6GYN~sf_X=M(2&dj31(JajX)&Dn|`@24XIi zc80qhb3u6@ln+kp9MSy=@(R|;bBu-+Bayj;%?v|vw1mvjSgS9LIyw8|`oK)mDJpN^p zS9go1F89mK#%diq_QaREMi+d^MaW@!LB|$<4?LR;_-_NA@;#0Lp60$9@QsWe`?UcUAxBx8@FC&l z`}>FB{rtn+^@DyNiQQgz>Q|-uWLpk!!`+A_iEp=PWqc zYYt8JQh9w4TZsH|+fU%v9Y-I>L3b0z*@*ln95?di$o(Mx ziMeCo)ohFbcEE<~JCWDqu|A2hR?1^t40=9vOJg0V$5_RBd93xwUnQQ?Sk*kghOtJX z{XUGf3}ZFwSV1D>9bt(H-oNNyAxFpyPXBMznV9MQ0hx-R+)m`n9z<>@@Jzvn z%=%hMhASS>6^A#X&i6%<>v{y` zG*iy0L}T>?wu)dOPk^QKjLA0QeQJOIi^7-v3)N-tNA?|`zzVVCFw1W}}VhN(h#CoCy6~f%YP;lcIZlUoe+1mN{1gU+yoEUj-e69Ek1n@Y}LS=*{~D z^0J^OzApsV)u5!_5y!be9%mq1upe~WbS#YYRsZ0+UN_nzH-BN5R%eU2$Lh!kWG%T* zWs32e4sE0j`O_`HzlbsR9D6Ussf_bA(0_IHAV1fE#uRSlk}}vi1%_xLy@&%}dC-u? z>6VE6{u=4nNpz@qg_xF2K!4Rc*gn#2bJcV)H{K3g*k&wF{{_k=9aiONtMW1WlQDj> zX(cI3bUJpYm1LrXU-EV-+s9*{$#1p1%-flBl5g4BJ9KPc=i~F6v358~_puhd_c%Tu z>X&(sO;|?vB;L!L$9bzYQ<^K&HFd^YSkDIR?TiapPaNc{5jG5cEoz(~)LH1iUZ`uj zok6}>!D8+w6=FSPg6hix*}{)Cle-U~9qj#k*EP$$O))1d`|sddwu44}-j8Y-H`QU~ z^afY*jM~Sc3j}|j5N{?Pli7BL`9d@=B{M=?sxFp z7695}PgT(GG`X)1Z-0huPE(ZI8?-p-I}`_<;@A)VO~QO&Et<+OUiJ?&oEnrD$@WSt zdp*jzMg6;Dy7Nc!?|v=CdbRLl0$Pw6%cr5azdBZe^6*$TFjOy8T*1|Wj`;}zEk-l$s(@Z2hxm&4UsI5U|1>wW&R4;18N*mX zJzzC_aXQ6#NbY&)B=y$@zf(J>`sH@Sgs7ja9SnN|sjn(6{2TBFYhPFg^ic78DP9Br zVIS>Ry!k7@+vVW#GT8KF+}D%#D&pL{G81=*BHR)3d? zMuzW8TL3*9(4rv=|=YxC?B{#R4U~}`?efMUN}PzAWQRYIU%oq zQW@Pt)aB;JgV#>-A+hIyjTj1Z4r>@6Da2dxF><>66PVki0M^B}_AleNhORaWLV-?$wITp|b6Z)|$_2Zk4BPnCMlPVhW@f9b3wB5G z3mj3lFt*CDEnu}VkrfP->8MPznsiVpBfqrk#mXpt_NgeWJ8344A0`hx%QcZ`hn z*r2x^+L8*asTHY#l@*4PrssowS!WYqbIZ80iaeYlrEy!{2u^HzKE!wb*>&9RiUhmh zN#nM@QH}atsHd`EGu5Y@O}K^XwhgTdqq=2h6GBiI#+>SQysqi_gg}(T{{fyT9{4)7 z7Mqn5_VTlvDroKGITmigI3TxS3J=vXsmzrLXIp$<)>-3L!D9tKt%2YJS6rb7-A2%y z0(x!S%-ap16K7*lM$l^GZn-@d<@DPC8jYaQ#zo&wzp32dOFC-=ee^vJG7H}DU_8RlEzkJ3NQbqDPF|4nnPmRS}$+};R(!+9P; z8+6B?-&KFb`6iuKXp-_Mk=mU}pZ z`#A2K^3Y!^=cp*>o~bY<3O%+&g1H|yz<6U%B5UCnpnd`B7woBkUFxx!sUEsc^=7~N zMA(!ts)xQ)eb`|AZfYOKdg64A&(DQkQT;uzVX$u%v>&l9D6dFeuqUdBN|3!RKJzq`E#} z&Jrx_rS*;aq&`xZzFEig4jVo^WQ6(_0#7W3Z_;B5ELplA>wgMpQ)p4}sQGbT_8G8m zUE)GqDF?m-`8FkJOK>sBQRs%xlLwyIa`WT$h(RZChHiacT>Lt~6@$;2oTj^5t9NVB zrU-il0lu{{Z;)Tcyo3SX8PQImL$Rypz(mW@_zb1bT=YvPB@Y$NsUYBbTeYZ@;i|r}5cqP7L>=`_KdD=f1 z$MBuT!zaBiZAh7o=F(?#`^+v3~8!^hM(Ne-)3a+suNM`ig~qR8P~&<+_{5)6DK zhbuv|S_Vn}NG22+B-x08{Kb;&K_)bi!!`q}(LxT-2XQqdi*zPN{j7ux+8~Q@kU5gY zxoCSU<)iMewjht}FSw1F+`)bx9%kwvvI6>zxHQK zGoklw5yf_#3ujni^HL$3aj1*K^E0vnKVF(D%4el0pH1)!Q<<)ra8(5*)R>@egoj})=tGp({o0}$(m4*ed|Ofm&SMlg`~R}e zBfd58cLn>1$Dj*|TdR1AVTI0QtA(wZype_GXG0GXw}$af>}{xy8Y%GG`?6A~>JNY0?lIw+KYXZTO!(rV@VE7aHMaQC zuBJb+udPv;Zvo?_xfbOE?M9Lh(`{LU{MmXl{22y64uAaN$K#Jb{CNBs3O_!7ss{Ok z@s7`*szLrJ^0Rd;`56X(YQb3Wq5kmT&sgxxA0GS}3%+$Dg6_)E{2duhHR|KfI`4qr(>ug@=BPEk9e^2Kl3wAG8};eoTY>QRHW=@tTIgkHa5- z`0@DT4?iA%hQg1}pHbvzRQ|NZtLzUi+aG=m`{NH!`ZYRys6T#Dzea~={_vuHjSgQt z6dwA;k7Iv$(f-i>yic`1N4t^bCu?xL*w2s8pJDLh@W&s1JpTB@kH?>(@ZXKb`ZfCehWf*c`ZYQ{^M@DpYjpVHq43bJvE^rL+sN{Rb|cG= zX^=mP{ERhT(=hmP_~Q>h9)JAd$K%gXcr|~nT-VCI~H|6NH`wWSnQRD^^bDv`j}l7-uPZUuL6jo1w7?=eUASxIwXThTI&AOLrmn0kPBg zG@KDoY&wC{cb8J^*H_u-AN$o^*>*OC@=bx;8dkVh$LSmv;I{$4#Si~nli)H~1Xm`` zZ?d?%Z%`h9CYZSho^lH*7a-2UT$B%xpz2F~R*y3VFON6_auB-Bh{bC9^o@8v>mnKx za^liEW7(2U(OH8S2ZLFu$YJr=UJ% zjCvmR{7CiwW5Jo>sCq%;7)kcIzTx_SvNC^KQI;}78Sh`_uS?LWQE6h}??^PUzK@6L zD9VP>gtB2Yp==mU{(Ks5&W$)XOJLd?`UTr7;%+Og(9ae1*{#-RMV=HHFfy)N_gqPZ z&L`=zKVKw$r4?=)Dsz& z9}V`a@_^3aiWv9Rc~E>4JSh1JJSh1JJdj|=p9KtIHuiAxtHDAIQPxS zfsS#zJGEB#SaP7_I6Yu&W61%@fC^5uuOrC-*+~_gXeUR6lkDDC8T%=W{nSm4`4o-U zJ%WuRxd(lzG*+q~kv3_p1d}ob42_du_)%fL`WW39iIHzNbQJl|FX_LlCFyoDp zV7|^ViZXnY^SFDQdDMfpvT?>pFyoDpV7|^Vc4Hnty2&v%k2A)!n;0X(j5kJt`8vmV z2xGjdwFoxIG0qqbH!((n8E=dPGwK*+pIx*$WBKgxZISClcR@JVS&jRGR_DG4&n$lg zpO3ka{1fnz%aK!%4gVc}>(MUw_xZ>JibKu?at)lB@T1AMCx2P(PbjdIGnD2BOFpz3 zR$0$)mgY8*UZb06C;a(-_?5oXMxgUA=~unKNyGjo?k4s(xwtP-gxpNtK4EodP+61( zcMSD*VJ~v47W13d^~T|2MD9=*<>lb6irPj~UJd-E?p>IJROH>KhVac6N!yv( z8;nm87u=nU+#<@Cp*)>L%mwX()OZL#`Pnr-%C#Q!WvER3yFZ!kc1X|v-UYfVK{xqW zR?yy$yk=Wib^}A+TDZvx{vYw z68_KsMrBkDXuOonmRv$UX-^LR1MhP?Suck%*f!<$rs0ze-dq4KUjP-CVYq^Lr9rk7eFu+YP-K7j|0Fwh4l+~b*%1E~H5NELMK|WMIe#5z3QUzXh zYXj1X7~6Qaxw&Ewa{nCG=8E-Udn&roZzcAtT|t#m`JCR?r44p#&=-|wp}oG)+DrFe zz$-Dg`)lUeA@J-_&z&OA4qd|k`HR0Y&kogqM}B-ebYUpZ4%L81-N3Kn+o22ieM#m^ zImUXQ`AmfaygMxO?#k>l6_>1Mz$3QgOxSZ3AA?U9z$fByDsnrsxbi4}{)q1w!F`54 zJl{vTlQ6%yxV8O@{uldkkBaU=v}v_AdOyu)ut7U;XN!m2QC@UC_V*M|C|ak)5%}(d zknddFFW9{~x3&}eLOt$K?a~_@bCD-Xzg@^{+x>QCEp(v;YvsbjfO`k~`9pxo2h4iZ zA&;)*9n{?h-9jE*Mt_9h#2qjv-FfK4U6O0KSE0QB!x2{hP%v^n%aI#eKAA<}j_uKR zxZXQ|0lYQgtmX0@%-Mr}>&?wkzbGuIjlsJ|PRxEM%Ea)@&cX$?Js6i6G?Tnu1neci z{&MppwJp$t%V_hdP5oa6%roe}2fB9|umWID$^9pe(7zu2ccBg0_f)_?vw3~(v!H?cxVLa_?FH>5 zk%n^6F!9=t&vt>`XLCN{6fok^=+=cvK&iLm>ZCz_|$RUpw<2_2xTTz!GULwMx z?pXGT%vw!DX07?h+{;PDcxEwj7jYjy1Mi}Nycpd{g;}++_{98J^RiipaU>B=#x=PMy_%PiylktF_RaCbCxCrM07Uo!>(Kr^! zc!+kw!(o2HfMzg$hRKyoyzIbQf2u*(s6`jFxW}91o5%N6|d^JG6ssBrW z*#r5$qBXeB0M-fE!*c&6z(M`DsQM@QzJN9t0bef4_lDZnMfq-udZzH!+KZ4)(y=|D zAr$g`0rGtWIQMX&j&?4z`4^DyINsp6jNhSQi*qX z@MkJw$|j2P9Shk*UhUB&Hql{5PH-}Q=g9J{E<^6;F@iTeW z>7JBT7Ysg-9lC^ja5Z7f`RMGXs6)$zTJi@UGoOl@q?u%UcR8y~1Ye2&-Dsyrn+t%? zN4pJxrF_&HwA%x?62OrSBKwnCm|MFN<$0SewPV>|#W(Cg%B7CWSz?*Y!i^u`d1-WC z@DI|xcdQrT#@VdkB;?(=oE03zbDzYrovh$Iz8eF%0v@@uIy`I6vY;#~7PKmDC~uei zAbMVl5xPm=vrwi7P6PD9%FRemy#;rtCkuO1w`o^nJgZXeEIbu@gZrXsJotb$=)F{P zv~y))-X=Z~v>^7&FW4peC`8X1tYay_OZOTTy4HZ!GL-3-YF8V1+}#P>h5J;zp+n23 zVeMLl`xkm8jz#p7E=xSXeT(*om+)c6aAP*^PyvSpcMvXN?r4k`MzKrxVa*M=G4h&= zd)+SvW4vXsOPIfwU7Od{9>T|}>0Nv)@L!zImR!KSlZ&`JDXw48l`HdwC6~4HZJ`<# z4xNd+0^i{b=K7bI=PP%xC0EdfY;T-`13tq$@Y^rqStbKG;8Tr?13uyvv>_Z=zmHyo zJQuCIH6H$>^EiAw@&opO#tX=?4}qO5*Yl2NU<2tl{FS4pVGALrEj6%(zW^MbJ-_G| zfO!`%mr#d!Y&nCv68JDA19_Y-wUg^z?0S)Mzj6KmpJyFxMI0wAK@7@9z9an6IP#g` zKlH3y!D{0I*^)TG5sjh@U8hOy870a{Vy7z ztGOy&J?y8e-Ty;foeN!^3)#N_y=#W9UVyG%#M=K1=It_c^^$g;?K1S>3^yt6GW3A< za?e2DFGE)^%evYOUA-Xc>fCtfpR*EsIMUT;LE|OJ?Ic-O>3cbJ@!m*w^jYX5Y)1=n z7T~Y16aDqW(A7PtCx3@@mHhRS&{eX7hr*>eMESaP@$1(qaR`GLhj<1u>hH6YzLC%T zI&hQE-a~SU{<)ausMq211wh|4-*_h~k#RON=CMs1FraTF!&d-%WbqB^yn47rDEs4Fj0@5f-J58KVF5Oy94k)$M5gUdZaG<2K0zv zQFm+t{O}(@kDl5(;pI<@1Kl69AW@J08}s~Ud_FCFxb}T~p57Yt^6!N?j!)6ZI{4Q++J%h7X>x zO3#Z3#Ipc|PwUyP`=Gxf|!5?ncNne3JJ# zU4y+U4bK`n$(*i(u;Y`8?bW5X^mgPhXD9akyEKb;=V})1#@RZ~=IL(yU7&LXp1H7m z;F$~krS!auAe7?{*cKh0*Q0S-VCSsyEW8TO5!!|I`zqvw;sJQBMr2q2v$%t)&)*N< zO4oPa*@*c2&RUn;{|1k`V%Ga>U9VI|dZCxUuRNi1<6eFE!V|o^iTJWQz*PpmwBfFm z=(CN&oALP)Df1;_C|^Pv><@u2)mpBF_)?|)=~9s|=fM{v^X7psWg=fRt~Nuxy)CHN z-raw>wQEkI%i8}{gD4c&lN*?E0rfgmTZWRxj+YEhlj|AA-^T>78Z{G;et=+&V}!(>lOCm-e7?V zciQQh2Ay~|1n#09>Fj?tyz}I)D2~(NISVT>M=6-QyYPGi>3JDy8Rzk=NS*hIvk9j| z3(ir$rzb(aCn@s17xG;J`R+n2HA8=?qZRVKJCXarKX=74C$nTnw*@gD!|x%oT{`%z z1$s7us6P*CB>bc3kBDE^pG4>l9^}!&Eaqs^9qG9WgSw;aF&Ol-b0XWvMBO@3O?fa% z9r@xx6IrDCo+8OPc$3~yBoy*XB)^~Jk(*g{uTQ>fxA-nR8x4ABxU;ymGcL*1eW2Fk zSkU5$+jGT}fVr_gRMwh`x!zU%D^F)~tEZ{|Q_Q9AMiThb)cbTg$t7Yah}#z1VJ{Yn z`Zl@2{<~DRkMsckk`r=)_gyE|Fvh}3-U)_c>|9vGz@5MBV5yv&-e50Z2AKp;U9lvS z{|uQl4(O_+pM&=VrDrk@zK@m0Yk^J@T^#5l9X$uXPmAYO(DMUDoyIe2YF4sH9lO$M z#GO=Yys(${YXdrqcIT3rGc*7;gPvh>hfoN*!_Ti43eUj~AtsG{k3n1y=vm|`*elX^ z_llQV^|!v_NzM49r_1d1FqV3w3UO^C?%2vOH)8O+=?jT>Jo=&-BgGaqpa;Jfo}XBj zzH=qpNBKRpS0-N&Ymn_c)|B&Co=$%yS+-?4uw^rtxBYQes5~RFx-`1C1G3tQ=clM? zBw10O5!a`BMqIy^Z>cJMX=z7tYb(+5EA4~5Wq}=@rp;M-CI_WJxa7$e2}mtZ{In4jYs zL;il0ksSU6ZSa0g8=k9E^O}}Lx`WEX>6sS^9Jinh`ySp`^n3^Gy$x-vN`JDn`tfha z^V3;)_2X0Ht4cpyT2=bu((2cKp}iZcGM77|Iu805^x_L z0ukm2b40y^^#yU`w7Klc-URH`Txhcl&qKDt9_fLnI|TdQ-wJPDSS@&I4;_bl`1hbs z+Gkm3pRPDAuR9KW$DYe=#hQcXjvTSPz3ZXKLKY6*yiRv%xdq@4Z03JpzG$puLm$F; z#d`3K>^ipyFwmWM6&Ucb9eR5|@iG?n7PvZNQ(Rd~8bsUMHmBIGhd$!@0I<9I8war_ zT)bl?)~OKu4!!~3KO>F{apRo94cgsZc=m-Jy4?l6-VMFBL9d~|-8Sg;Zs>Fubox~P zb(HZpwxg{?d%YdcSX;%}8|?EyM|%)#KgRQOg+`3SLiEXFXvdmai8``3VVEa{Hi~!H z(03gAqW&G|k7(#dpF~G9YzgF}+sGMiv|-!^$l0xs0s0;SJk(ZAH^GuUQrjZvx!ATV zhh?-+0-d0V18rFv@Yjgv-t9mJrLkcislFX!?*t6>^(p>~^9ufaqI^d^&0iaSi!{st4Iis$Xy*#jRD9-EHrVf1 z(}4CZXgfqk?rE^!MKoXzNQOw)8UaHz{9fr7vUF1UPIQ8XMG@9r1A9F^Kg{uvqBosbOMVDE&HYb@r*Q^@ zQf|TfHZLO9v;KQ*70$LWToyUkSBcoqFE={sSXc=3X`Zj~`5NfuyZ9h}-BSbIyoC7I z#jrh5nds{@7n0`2^KcB<7rg_UQ4T$XeCH!SPn<1sMGrr z&tA|PY{=`e(epNVd@7;ysW^x0jwqvN^0@i1T@{B8E&#rkrHjyp;!yLr`j2S5A^4B8 zxwJ~okdNoKMIFW%Pvb*lG?E?iabe{c=LXmmdag$$7m|MweO;JZDURI+JCu5&vcd)% zv=Y3ce)Fdbdy7DW)qI+s|KaY8*j>A<*l+3$a|`nB89wiR~uxus;stF^0o z2kbcccd*su-?_E2oi1Su%1};rnlEJO;~ zG{7M~(*pFQa*2EK+!J?aY`CjEt=|4;X+3q+ru7!{#;Pca8boiXyB-4!}@;gbThh*}^=m1Kkmh3LD0o z2l~l}M7*^XV~2kj+?@kDaQ5RQKf(Zh5Z&}_3EI2U^Qd{)MOvF&2f3DNJfD=#eoTeA zeORMfcvDt%6=GX1O<8yq>i1)>e?X%%Xz@(BwSYUUW&8G*ZrwmO|A6+peRv+E&1A`n zPQ)iBehuPMc&{_&hT*$<9rzHjloaq=SqoCJ7SMBY%KXYe=fL_kWtdDs%<|_Idu914x8tG3r(M+_{T>8_Eb+z?> z1MQz_Bk?>zX^n?Wx8}ughzmyA;1fIPdFXwJ0jQscF8b>5zp-Zuqdn73>_6V@!}}si zDP|aecvK*s<2nJ)a}B~fCdm+S8%}>#9a=9k$ge4&+WhOEct@ulkNXIVQ&KC^98)KCoz~U z;xX3TS$Ixm!u@Bh!hbx^!zZ)hlK^|f!#{RmZw|jU3U(w7&tZTj@LX%h`qGo-RN6?@CVO1#9-RBGu-FF zgSS5Qcse3jT?Tli#D!>_+QhJQ;;98=q<3B30FBh1+MUC@&d%R>G5tDze}>=7(Y6ou zUjVKR?`tbdVBzg}kI6aI5uT~Q!HaPvz8i7j4B|V*q=@hIexho`hs1jvh!LfL&JNIL z#C?hqz@F-V5p$r=FUB46*DyNuH+)`n$iA&wwiTweUVb60T2YZ^8LF`V78ChUI zX?&BxZ?SJ~S~txno?CpQ1A8~@ISP2}-T1Jwhu#KEBlc?+#v2;&-3r`24dzMV@d3;c@wpp#h{wch;xX}>_&g0fCSKF~N8SR?PRK6tSdmSV6XS!}=i`0j z8sW88g?6Qn&oLJ2qYvY0m1!kiBm6T&IiR_u_W{jtzXJRRgx%0jDL$99P@#FITXSc< z=j64g(_e(HoIB79e9Zf=z=!|ttBAePGmXzDg{3QW)BF3LBOb`O_u%`W49=8gFam7_ z?^AM}Bx94sF-y-smd5>K)Q>oBFPtx5D>3#P$;3Zo_xcc0s1ec0CRILVD4OSOwXE7|71g;D>ZzJhb;U!(Qr^uyE4J zHPBDm`-Y)E|GlpT=R0KYDE|P@;U8kJbh8HS6`nmW+AD^=tsZNj4r^iCB<8bXEwsTO zOvT!riFMnCeQYE8i)(rEtPcJJ{6@nXF7b`2z@I7aZ>^|{K^fT=qNx*SU1Uccc)ocm z{F7AJN{T_;eJcx#gKtQ2jJd$I8)f;pM@nt;kUy+JTZ%Cwez^cPBfR6JXnSqM!%yHR zlPx%}DN7H+ToYetEg`<>!Iw78X!CLj$L(pD;eETlsCQy7lfivXOdZ6n|J8Ql5o;S>>Yc47i z%dNmg`-GV&Ghk1F_>IGiIC<)=fCZi@D39AjXAuTbX31w1-@1JHB z_IASe&xh|%v=s%0BHdl`?@6bKPWa3sUd*e5&LzcINDgztAMvJ);M;e>7L$M92AKAf z%t^leGQcH4E{@_1=$tlL^zS)~B)zuJkvPn44y3w`DhmX zdGHASAQSOnPiCVSR|@|>Yr7b2X-?qB+ep?Ge>e}m74f1H@w^L=Ir8T(oGgzbUXV`@ z-`=(kz8CrRWE&K|V9pKjQAOLJ#J;GG>;jc5dT=x26!$C`C$+O`1qn-jTb85iyn>I|Gd?cza=vz!GR==^ZrgZZ`fu&u~->OtHftZFCgYlHlx_4hAk zpRoDr@@(wSyT2{em2MTD&&p=2+BL|v1x|XFa5-{aE&=Z^&ud#QYC~+)_KG$H&lIJ1 z*y-Kor-Z(S!Zm%N$QMGsS&Nxp6HVvkiNM)b7}tlsoye1NQvAe>Je$kthsJzpYjJo8 z>>1&^qsij9pPQ7n4j+T2t<|VO{shk2i3a@}cs73T@o?Rh5T8!U* z`Dv>@^oi(aTA0HS`bcze==x~tuQ!Kt{s#1}L#(6$c)H1d?weHbIq08(^Y&COZnput z|3ED0EngFz1eo0|q0Pp3LKj>JEKc`gyyx-W3$|3ML)!%+ZD&4hy@I^@uKBEPH)su> z*V`(TXGQmCvx2TT+<|y2JKBi%%uwEI2lB>q!Hb zpfMZ$ZpVk?v!iWnQ*>2%7)zF7)}HGp-=0I z!+U?WEUo2GesKo!DnNf1Z0W6#A%drN^?3d)Gp&!NdN=lS$l9JjP9xiAz&Pk^0`a6C+*`^BMxK-bcYxQU4}+;H{d2^V zs6FM15DXst;%vvbm9ds%kCcFSVeN-3XD!K!&cq&vi_eQ@Gqa;9)|5@>e4x8)j^H(F z`1}rSsBHuKA$!&Vde&{p74O*?xsYzMb>)y#f+aYsfi2Q)!Lz)fFOb<57jpZ$AcMMX z0%BsJhHkX~Dc+GyGP{2B*4k8*=i}WuC78Dh(1(kp4_o5;y5Z-R>=gQ9wrn8z??FD| z5WT2G%nJ|%_H$fxZa=fThx({j{HCQ-ESJ(RopeWo9;2q>uBX5cgzI2qIDB8L-I&@ zYIqNgjo#}Jt3OQ zv!lr$CY=Oa+Ahce!2+&16y-EGq{nmy?J6n`*TbH`cS@suIDZ7W@30}1r%(AlkT0hN zHiGuolICG6@>tK|zrshd(-$$Xm~XEI`0y^Go*3vH;p#$c>Nb&gpc^#s+qRuG=ZQ9? z1@ALDLN>Vs?K!;b<=kt656=cK;DWR9#_X(UBi_@8Tr3}Qt}=9((++BHW=`;@g+u?O zhtz(*+@9JpY8%M#`Z|f{-RQpwxtXD$f&Ve=8*q%)ztSH1?<2pC`tPQ*akSBaXT*Cw z=El4;voD4de58x;L3)xwLsEQBv>AS6?9SusvxTf^9yC$=Ht>}Cq;|x26WW@Uw!|mI zlg;ooX&f;n$JYyTTjHI_FS#A{NpnTKC0M*~E~6d%IsyB1?vUU!gMSR4Bz(j@6w0!7 zY(tig`2?PYW8HUNz2HD2@9}ufa zBb#WcFW76vIeHA{;}m>C@^3SlKA&tMw?udY*H#AJ8HDHa$8~8nX?sv-7Hk9W>#<>t z0N)FmQJw&tH8{hdJ8jDSkM5vi*mc5d$WI0T>HZsR*}yY#ba-Zcmo^6H=^9(GsTunZ zoIAt6w&8ml&Q@W6-Meu1V8+>jf|J&Ux$Lnw@;OafInLAbfZxFCKcYS7O6c!8+SlQe z&qdvC#JfJMI8#qWZciM1c*Mu@^|PSY({bk(JWN1;bY2emJ<@e&WyQ+Crsqvq`_(j3 ze~IW{?8ihofSWySI15qeQS$5<)|RL6?l$t>dE^lqvFFgpzt-<1Q2VYZ4C`-qQpdio68fY2i%^?T?@9oj zeNmqNF5u>|=ghtJ?l?hwYB4giKPb!v;R* z&vD2Y`LF664D74i9do37Tf7KDoGZ#Fq1+N_z6{7K(dX~ReEH{El3NiK8NSw7ilz-*oNQ{@2p|+HclWU;SRZ z>*{yoiHFr!XU11)|8ps=yOalmGsmuK*Vy8*?z`s6(yY8u3?19$SDt`(4^kVPh1@6wuJaBCnwXbz7t3%K zqu{38(Y3%q{-hcG@LYtO;!S0U2k-i(@ccTwi_Hk0mo*f$X6c0+rs={BoEfJ-@cQZ>Sq<-c+J5Bq92=p{hXZX&#Wuy?srHmmn}0a#DSqIp zr+S1oO#^LSlKLt7t>*;V2-E)LF>Ua9ULK*%TLW#ji*4#p9QK$om!`7!J#A}0_dGK~ zn_tRp+Rk%t5Z|&k?e&zPO(|eC?4MfcT!GFBlhS^Jc)Y~716TjcQ|fy${lG(Ccq#|_ z{E0NS+{2y{@UXu4w1?gOnWtccKKBl^DHq#Roq5y4VQf{pk37Ph&pc~KXtQ&mjWkzP z$A0cfL>rUocOKKX`aRzsq0Od&HV(0$Ebl?je(ERn1CQyW>z>pR+T;(k*(oR|PI}tD;qllx#f?p0_*a!lt} z(wXP<0r^iwopQd>hCLsh5uO9@>1^nhA-L$QaH{*8gzMi07wO)4z(o#$k@{OC(2p>)PF&Fw?%nP?-K7}Q&WpX~k| zbsa(23kI>pT}GUbLDwm6uh;>KKkvDB$p%{2MqPyjx$g?&!YL`<{eXUL0M^`Kv>Z>#255Vs^K=+B}Y;{fHCLiL_HWzKm z0B>Qvo;x|d;<#o?w^2LwMl#MX_LnZLsV;q}*LC&5_()$7&N8NB-DW+mtJCA*KV|IK z{sHftDz=j!mpJplz3hPo`_q4icl*-41mt{+@1uIK!M+`3N{(|Z^or`a?={$Ik4Es?MdZ+tpw{qpCZguG`aK|2I|L%j!C3gB@=R8yNdOb=?aM_Bzyw{FZa< zxqoV~JJc{gYp}nrYQIBWx4*&eQq^r!*S*?cKcK4np}Nl1U?<&G=HpRyT}^|1pQbLOF(^ymSl4DGBkPTJ$#hjMYA zphJ{D$xb)e&8qslly>4Bg<^X$e!mdN?v&P_>#be=pSJRW5s1M_Yfg~{|Lymm#d}^W z_(k{a#rdhDKQ_qKZmhSrm*Smw&{G}J3b|-6O{|%(;KzN7ZMP53-A?$V;yj|?Z^$sB zuKuaP?p-X`VJuq!*M|ETmFTzQc;XWAeaOeTC0U4Pw9TluFMy4Rn7|fNTdoCrMbHoq ze00xZJ=)UU527Vc=?^^kKE8`QfgKTT&<73nZ-F*yL-#c_a{KR6d%8yhzP~TiB+~c? z(6|(I(VY?CqdwkI(d$fJCG;AD~m7VtPyALI^;Dm#1H=!Wn6t$bQAd3wWGB)=EQOzeRubL>Pf^oM9kaE zeYy?b+e>{>IpP!V&Eihlt-gJ&D^8>CAAjzlm|iFM@1B+CKK0BF;qN6l6V6sNd{%Ta z;;lQLJ9*#l-~Z&^9q&JU@9e;ncB+4B*|bM~hjOt@CzTPL_&a3i??B)vMcu40sXXWX zwf8Q2>11Y(Wo>i-Vt@zvoM`%eDi4TaoLkBll!G8;QVZ?sZh5e%j@Cm?Y z2LFgJcuv84R;+_L;F$nCN#G^%kZ_x+AJ9tYdpz!_QarpG_&LNW_Zyh+yC{#rJSljV zwJ-P4_wWCCxo>%s^qu-8ey-)$MnB9y9KFNx{b-s4f_s&|cS_%f)VHYWFOE?EfvUb| zg!(_I>aPvf=b-=Pf0BL^9XXbVqi2J*WuT4bg62RAKGU2eV@_xu4&Zwd_+Y|!9r&jW z{c&^v<^{39jitdk;{1T08?yj2>io>iil*~%hM20@{=b7B&B0t|1x$N{&75^F;p4z_ z;=AUh`b?4oGyb!HY5$lAUT1^HH0P8PdKGc+ek~`(E=q^q2TuK>B;D2tJu8%QiAc{Z z+orBIp5_WDuQP$mD!?S&X_#TUKngR2wJzCy#g(oezveC-K&wRyCbOH_=LT61@toBtvAUh+cnM z8DvX2N8undCd~Ij(9Yv7w#fSk(j)Lbi2YWK$Ex{Sa=TLxSaI&W+Jcpc`?7dbG@XGC z85j5UjLUxxlrhox#u^tB)(m;}LL2P%ZrGi+AVHJ?e|aE2`tzCeNX_fNl+R}Hna+y- zfO9K)Cgs)DTI?^RXL&}z7N*>{C%uEK-k5iT)*?Q?5qgn}XWQ}jAeOl@X_fI}P=SEl z#8KIC!FabMjUZV@2^RAxQ8YAWzb9R*a{!{92$J5ydo-t|G&Y4e7~}PD@!la zO6_r;no<4h%JgbckNEs^xVI?cmw&g1i*e)kxa<`^{s$ec6yxT9(A!G$KP66wCvh#Nptnd*%7sa|qh@LPt-9)nhnyuR+S5poUW6YO|vkl~8rQvkXT<$N4_xiR7IT-b{k5mIBs<|8x#-#`!hnhUlvP0cY4aN5$_>*a-$6 zxBoxvy?cCB)tUdl_c;kUCjpWW5JWsVAzYH+9TC#uG$#paNRUdvw$`zPiwZ=jVEY}d zMuM?&2`Cq>ZKx%H7S6#`RIFdWEkk0eYdz~(&w6g_VPn3&$u@PK$Cxq;n&B!hqA86P z#+qT2x$~p0#~MTHZHTkHz0_12n4)UPpRowCMp`2K4;m4804iEp!M*=&nH5T7ERQT?Ypvfmtf zqic%h26ljJJU6?F8o}pua9OdQ6n{nS<6mbl;Wq20aQjZ<)KyNOmAjnrg7=$Btd@Gi z;{6;wv6p_!2*gi~FT+;tZsYO_XinDL3#N|B95R)C!=lX0n5n&u9~2knCSOsAZWw<5 z{n91pt(kk{+`Eb{9@4M0%zHs;S<=+G+?VH0Eb3ibQM_`vCm|Et;>@yy3o`kKK6}cj zfSWYDpC6CEasu|p$^GV--h_F9!UXj}YJeEh!;R79w8>AuVjonfI5M8xWPEmvE5!4p z@vhi2(Q#$0SWteyHQznu_>!b$min3dn^-G^XGZQ#Wsa`bvwCZ>`poa~TV*(H_hcGV z>cJ_ePn3QNYjAfgsL{kI`52wvW^+w-#pC1mAb(CFzSEB1l!d}PZ~gNfhrFhDTbnxt zeZ1lfrF2wacNW_F>viE*e!XtnOY;uBO24h7-%?F%AsOk6w>?BU`Q%?( z7Qa4==Xd$`-qkch1>$83ZFg;Y(dp&ylN^A=%GnXgq+pBzLpDNn3ZJX(>BHq|D^Tt$G zRd!C?_SBeFRm;%#wxzp5yU{HxegyM(M}g_xi=FRUk9qyGUFl{>vWd=*v2z`**wS{~ zt3%@~`n_FpBXfRz;8tW2=uEHN`lag+E^~Rp@KZaw-2L%2dheig=&9xaD=v_Q9}DX( znXA5^U9RL27OnbDeP9i;63IS3{91lU8iK7WlB(!aiFBE%n*;MyFefOcnQ+FRqIKA-DCAGEdgcE&g7am$^e)rj=U)*u% z0Jw7b{pYPYVy>)ujj_AvySLw!;96C+pP2Da*X>)E48GKC+_!EuI8!oX;o8Leo?ibR zc=BG^_Vr2NjGl=pTePz5>GcnTGq2|ETlX$_^EzYle%Wj5iH*Lm?|rYWf3NI~^{;^! z@9Ez@4M;My8s^xv1bzGmYa>(cIfef{>bzUzNV*_+A|*FR0!ukrn|>e_Xa z4RCGD!?Z7H+xuNh*XmjL2Hd}^eLT4KJ<3VCx6k@_H@>#cxOe~h*UOTDfcZO zmuz}{U1Iec>n@=j!9XlU`wlLGvsGUq|8?1~tqTSwAA0w z_}a(JeNJ&Kq+4G=Jt5})(Jtto**9#0--dk@T|hC@kByUi442vj|!?4_XHe+NXP=7yIhY ziHC}yfpbINeFud3^IIFCUs7%g#J{rK0Q;3El~+PX)WstYg2?KO?uX{@npU3 zUSr@rt66_!E5+WgEy!~Q{xiHE*E{k^i^n#Riy4EW^4%J-ab?fu^cOZ$2YwdSk5D)+5L>MhxHHJ#A)95pGFtuEj4(S4F~Uw^lsNl+(WC*v~E~u+~P{~ zuKwp@0~wCWrdPc1afD>4}q@dBf|$0XD}CYjEd z_^XREBV&>o8I!qXq4k-J9rLBgc@}SwS=MXns50*b^Kyy*a%yw&oY;Z{){TTwW_&_s z8T`S8-hS96W78gAm~o>qy2ZV6FmUu^jIy+zr*VldaK_+l-cWpm{2VPl3Oe05+E{a8 zjN}Zt7mJrj<6@qb&EQhp;dAjxD*HNOK6T|w_t-ad;vUXoSghD(Nuf?;+STqKj(4F4 z&^+FH_{O`O`kZ)nOBj z)?LBpj&&y)-JWB%9nn_F42Th{^-J|OC9pP@)d$|ZjF@%AGze|(iKE!WTHAcsj-{yd zg7JP}GnUi`P9w|zS8z~eR9Bb>et4wovFf<88g$QDesD~7YS5!cq@Qd=PxAu)Q(Jq% ze`AC8Ds5&BA0u5s1~kZl#fqtScYUC-(x~}1dDRz<6~UUVwtt$Rj_9Wo;}3qmJI3D3 z_nfrxZ4PC5T*es7?ts{Tj%~u`OYs3_?zPqvE0`F7lK;y#q=i_3E$PffY)A0XXveOr zU414S1lXU0>O0w1<^yg23O?WW_oZ9V5E*8vb*jOwh9dXwODt#R`Y$`+LZ{k~q zkzDM&E1l}n7)Qq{bl@C9pE+suU*##iVj6yjK6{9K+7EQz6Njw<`+%iiw)B1p%qQWs zY(vfO)d%8ummW;tjOF!#iz2$PYWJJtjh=t2jaBu5;ZgtE>8D7()yA6ofG_Ib@#ue# z)&~Yg{cDT<_rv-?Qq;e z_%Q0N|LXWB8bQCTHlC>uY^1DTM5S+tgWfz}9}o}!501_hep`I`Sm`O@ z`^Sg}C!OemV0nuC>i6oU_tk_VW$F7UCqH>!jlf{rZ4ztM?OOAzG0^dP$#zZjk}mX< zZfsFvuu18O&)Jk{U>{}MvdDHxHcandBDl(20c<#rK6<7EB^>Kf8`|9#n?mTO(JPoh@^my)RY{B;a z+wt0Wj~%Z$N1oTZ^VoKK{!6Qmr(s`L^1|!KgZCXjUVV-{b!X>!XrpDjXbi4BUTt<9 zUw4i?JG%3%wr$&P--tcjW5u=Sy?VT)?xW)mo+HneNFLdUY5tZ>eblmTyXPEfe?(gO z(<^p@=6s!H`(@iDJ<31PRM{`-Uw`;q(E`Wj^ES#;{EPO;{{qFgu>PNK*-lNj>a*=b z|B+|M4$A3wJ2u0R@I8VzF&OHxbCQkRfbe%B^=mwpM|^f8_EHniXoqbhHsOqKj*Zhb z;M3LM)dcV>A3SsT3Ukf4D;?Rwr`rv8u)Z`w7abh@*d3E4+gY_^vS5zbzN-Dg!4HWK zUBGvj@xVdH-fm{jHf%5j54Lcv2angcFD;(A?&R?=TlwkNOMY&T>(1AX*AY9!|5nHG z@IX%692f_6=XuP^(@^rfWg8lN5?jg(PafwirBCEZ=+3jm%2U1pTS{=$!RtSq zBhQ~Nw##vBSPf&1WwUvZw88`7<4xFCe8^hs;HB*9@EZ%iM_xN_#sDj}ifz{@oRKe@ zaCb=f8{p#Gq;q(J2Dcq^MR@h%IpFynY4eFoq4J;6f98(XCB>_-V~^PJDQ4dA=;VznBwh%4ojQfDDezoQ-#W1EBCmzh&itgzC6u}D47lt$up^SzT8HuBlKtji zvdz1HbOf)XR)E`znAL+lcW9(jsCZ%K5%W+zYC)O?XM36qyF`e{&%20 zkRA1}PxQZksSjKc^{-d-zu(peMn(PWqEEZq_lNqxu&95hqW?A42QG~KONp^;eO5QJ zHfrrN-ezqjt=2-VYl>^z63N@ZnpYa(Gt{4l`K~;-@=bki^>qvDEcyODl1J&j*`4PP z<0S|C0Qs8M%CBE$5YPCJ;}1mATI)CYKLTIW4wbXFyPPxga!wmERIRhf=bA@7cY@+OIb8o@!25n3n=i>DctRWos})wg#O)1g|~P zS+%DwoQQvmiO$MIUj8WGr6UH$?dUpIyJKrD+!}W5+RoU{Yo$}Rz9)IHp*mr|%l>56 z>bkFtjPXo%^z;4j!&5s)Apf8B>pLdraep9VA$s~nzF)<77W8Vj^E&Ad|NE>3aj|wD z>AZV2UW@MN{e#l$$Km6B2X+{ijRHE>;U0Zk;Omujt7Vh$*0@`+9U+gcquri#ez5Z$ z^pVnI_ww9o=_u9iw@H^rx>*@__%@*FO1gmD`D%mv zw-&6c)gLi`u%Cy&eQl@gl=Q6^eKXST8!L_aMS7xqbP?AB!_~kn+m;Eya*bebvD@zS zk=l5z=MJCJtB+oxkCJJ>>h4F|-zI;Y-CwI`@qYq((rYN|a{6m?#w?%goE}g=#os~R zg}%!?i+oeKHdCI`{)x2eFZY*b`Q9J5khJs_{h|8a8FvS^HrPWQrN3kBJF~jO-=@8AVnZAp(uY}H`w;xl2Zp0&8ir6Muc4MTM-Iz(8(!r#0b>8FH zC);*2+J~qN6FilkMS3Zfb%H$tGK5Z(_v1Vd2_F z);~YzU9SZTM&Png}YCZUb?QA7^{u+c`ySTncz%c40;zwXD65(_=W+48(36k zKg}Ot*Rx=Xi@>TmW5YEAJSy>x1Qyvs4Fkqrz@YSLz?>Xuqc!h<`5kapxa$JGr1)EW zKOw!~amC}i3w(#cIlT*)WV4dS1#ay8J^deTOSB1EB-o>EiC*H{)(h|=}H@lBSmu~fqppQgXtT`6x7rRepS>t&tX*6c8ct?a=*PaB1+PCL(TBR=Q@E!vuH)7F_5ZMAVJ8b9|& zaM8h0;p^3mO)ncaS8Lum_^J6LxSaVT9Mv2FKUep&adWjZk75`H(M7F2qH#8yxA`Pr zdy6(|9PBZf<*TQhv+?u@vUbs`j6N2<+j$9nuX;oSq-WMM?O5#s*7OLhdXIrF!~(Pa zj|3)t6aHw9>)Ykvo7yvic4*G2zc2Sdb4AN-9E9HA;(M9-e;IhAHO!*(v{T>Jj^SCi zS^3iVKNGw%fM4y&V}1#z^S~pMXKbQ&LWg&rEt_@d@J8A2Meq(fyi;`8X_w}Wd9@9* z#w0RttTwCtk?~p`p(m>^qpdyjL^N)OFOExe&Kf6YJR{?!cE18G6Wm(glIfo&`sE$o zx6>aFFm7s}#;A#YsvvFj7>VYY!2T;>X6@LiXPwD3)M~q5Ysbzy-W^?vWZ7lNs-kt^ zUGlm>}HKt2Q zt8_AFY4G z6@Tfh78?f32EAT9YmY2QGJVN?#j9$~@7`k}>y19UBoAIWT8?YbN7Bnlp1P5IlH)$v z+Vxl?a$NOG1N+Ovk+WJfpC=*fZAM-$nXdZrFUey6(pfEAsaG_s`>YnuvC~;CgW>V5 zSS0ju8t|x|Khk%Sqbde!bX&&JuVW+sw2zFInMS93jdVKeB{K`(w|ZN~E4i-DciB3N zb6?mqHX`d44U&vEIr_ATehe9rui&?)jtcE4f;1G8~cxO-#@tQXD=MzedrIzA6Wg; zS2BM2&hc1$EtIa;-FE*x`0HCIKY^dc)r{dS*u%T&7x`JJ4ZkE$uiVHkgO(7rbwVgH}LE;_YKT{ z+3LTEC)bLH%PgLFfEExvWNc`Vo|`~C=DZu1NK?fxHj0KWWX`&si$ z^GIuEI{lxP_QP(<;~ zt25K$x^4C~wt~&UZ`V0skA~Y^ruoghIvZ~AM0R`*PdiaQ4)C-Wo?Z9GUVnYv^xe7Y z7XKjLTx~g*-TW^s+7s;F2Oq)Cpx|`ZmGDhAzjxH`Keu3o{+APT%ZT)e29! zk*;y1ZhYpnAnob$2nr*5TbN3JRS&mp!IXJQ>p{1^^0`-o%c+_ZiBci`H{FR`6l$z_S+D@mIeP- z&H0xrIR8>McsidzHoM~Oro-25q?~xg0v`93Lpp20!#|yWsWUJ2O+0Ht5)wu+qnt#MNSHFNHy;sa&Ayqj|;?KtSN37bhBvW+^QvUFZWLS1leK__;f zoI9!bMHT!H<@cOBsq>WLkbk>eJ*oBGyc;4T_{=ie(z&zQVSl&^V~cUd#Qwk1mpX}2IGuQlvW8R zw=9})er~6p{W|kDiAywUP#SA=nlIlo)OTx4Oy7#t6^SvP;l8Uc% zc;A?DLw)uCo_@&+>Ynv)m)%!*UH^U)C_6tR-M8W!>x$z?zOcv`H_VsjN%zf?{T1o+ zB6e72#xT}}bp8!xeA9eOc(!0Lp`pA-_lF-kt9}q%(chgu7JYw%K9TIy*(X@@totmS z+`o7h&cvv*aEK>%(1~N6r!qg|EF97v%)$;Z(+sY~#<6lDvEM{D|6e%^M{SaAHU8{Y z%(MT6os0HhI@`vHckRTw-YWX|H#o;E`ivI#m63Cgqxj z-gxMh2O6CVtrCCc_?S92l-3z__{=Mwq!*jb_&hD=xod}) zIZ@!{tmt{o2lap1gXR6wSTnRf$d)ye{Fyi8CBKS}&tNUcTpmb%_4JlSqvSI=Wnu!c zb`uJhB!qLxBYHI<&8Ayd54Z1yx!yJ6d(9*+trvsv(Wh+ft!J}`v-ZsUgV!nV_t*nI zf#3YqKdcSt{hG(M|GQ>uxpgLh{QnaCQlB(jEx zxBaWvkcnSl4Y}_B&KeSJ>#O~j-Jd=7zfOPtSFO*>Jzso%&b;YyVwxr-EH_?Qn!$Yi%pIFCljI84~N7ivO;ft^1&qvnr7ytTo+*!lZ%W^vf=g9BbYq+)djI8Uf zyK_6O^?gZh=l590CBM|VZt3o6-_N-Q zE&l>*(5nAFYmoMn4nOp_-~s0L`g}Y<^!_}u?!Pf%jn(IGeA>SJmdL(*!lSvJvwl6S z@73Cuv)7Md4bSNKadC6gPZqh@TW@B6ZmrpuJye{5JV4*HSJxh0Yjy?e^$gbjG$traANu$8`@hk7w0~c}|NpmsPjsC+=tcID**WJ>PFn9{UUaWs*ZYxgoEy&>XZ6Gk zn&>`taGTcEC0|;j^jV#Ai4j1|{);E!5AWSqc>cX><$t>TVk3pP@|CP#Z&MA|@r&zL^;7484=9s5m@MKO}t9SYP zdXUw7f6#T%@<*mV$$G$%%5R}7JD1Esas#LTo&EBEs{DcV@$>K}=UgBE<#Vo&zx}US zA3rZ|A$h-Il7!E)$8!2p{hO;eB9_l^AAErNhEAU4H@q?8H{9Cw-r8-%6y8`7s0pRt z6MF9OKi*Y}{%;#`g-2maUQJx#YIoK6=WqIMXcuRf?RCX-rguVHKDv-^6YqZxx+>06 z3GdTREJ3~}|KB&#Y@2__XZa*P%O`G{jNa`!%WpaFLZkmc&cXOL ze#e?C4GBg~$&k&H%@ehOI`X>ZFOU$dIgFmM`OVXc(mAZ4 z5a9N7?pcRIaY`os|4LsIN!y#WM@hTV?9=uZx|d~~H@};h4a++BS?P7|gwj97zYpC5 zv(7I63Fc@4c`}IK(9AhnEu4cCk1lU7I!4v0`c$vJaaPoh4K8EhJXdl*o$*m<_RlH^ zT>o54x;xZ_otpag)B034QtC_jWE;n710LE`*<>c;3m+-N6|(!fHej|(4{H09pFK7+ z(q!MY3*EEqN8UepJNnS+oJV0`BO;yT26Rx0H`eS<&r0PiAElXK6m;mDbf-!)oiu*l z6=z~T*L1@`$LsCT**!X2hVx*;S@=!$_83=AH+z@XpwB*vo>e-n8v4~buZ4cmKbOyKEzV#~ZYTY_q^~jiwq1>Fylg&} znXd3Mbjo=hUC+-!&+F;v8uOsP$4^XiaI6oVHREg;c^!?6TPyK2QVnF;v`0ff`4z-WX>be*|l$SR>&pT(Y0{~&tBs6 zOghyyW>Qk^?|w?YUFbAdnr8B=$t%G#Gx<9It5pw?Z)H@zrj%7xubRC%1F3iEQ_yv% zjYo-v@&K^bpgV7K_YNOrURN^572ikwxUZ}0XF9)VdZ%%@;(3hf=wi+q3y*^LIv=P3 ze017#c6%%6`{?$5;=k6P=>8zy2eCd5!mEbkiM5eKyF_Es%w<(Ub3^ypLy9>eSQIm_ zg!9Ur`W?K}8KNtIADe+4e*X;LgUkgh)`fjO(L&ivj^e*`-kCgC@_iZS55}Xj$Rhqm zDRzu~cwWUbwg;t0u)ir{T%QE@WY=tHogLJ}S6F&QzWICie9PMZ?L&&owSvA>Oc3l> zr|hLKb=FX!=_%r@!K${?U1L^}PUrfqB3%vXh(9r9Kk0OCOpTdPw2E_R1OsuMroG7; zt8!GHzU}ARB;cA2PJ_4Mi@Lg=m#smwJ0VN(&gOqB<5NUDl_K$T)_Fk2^F`#J)On`` zlm2to-ww?kOTJAzoc?Ts-soIron3SkeG9apawV{%Ltj{Upifw5^%T@qR=L0XB>hcQv(P#PHr#>BX?h}dnMOUC(N#+V}VPdBe$D3}L; z42(mT;Jw`}nPT-lW3cjO#$)*2!OQA62bA*~6`$nn{&&MOxPr*gxk|7hx`z2EU4_Qt z6`l*w16leS(^DAYeBlzl2PgIPoz7iVeL?JuoU((Ir8}3x5+-Ifp{B@_(ePI)cS=@&$KzD6Fm|y>}Raz7-nH} z%w1!hvXmy1HO#TauH_6%<<)wjf8Ok#e+|Hx$@c`q6(-J3Rw4hGU*W9NT{}_@SKAEI z=dcb|1Gi$>C2&m#|I(mWbBLXk$)$9p>1bxHqTLHy;&fi4x6soOchv#fPK>2>jNh@e z?G;r9=PWtxA!c4m&c=+=y{EdS$sfR(udX0Af9wnV+(*zadj5%pC+O;>Uo|~@L(nPN z+yp0;bmM{QP)u&8{2=+A=bo~$Rl{aQxM-K{kCYvagQvy@NfV8Ol>Mpr=al`|;JGnr zXg7=wo*TdD`SwNe+(kc~3%_4t9EFRT178Hs=b-OC7oLZ~DV=r6cpod|S>-lI;dvOm zdOK=N&&16hJTGBQI~QJ;u&!Hp?f!JU4uXT9hSw#UPv^jEbmSIZ*YRI?y@WgVU-+@h z_x#NH=HTtD?MtfiwwG6(qCL@bU|#$DA)J_py;FVXY=i{QF3N%5oYGY5t1 z#xHu_|D(7bjfcYZPs2e6*Ns~Cf?VQ3a&^yu7YQNMx*4!#|}H@RQ!?^0dN19;x7ek%qa;EcWNiSgOszAWptv^T0!UEe)^`H0%8YMyK0 zeUgmi@DzCBHx_vTLZ`Q|7u~EFT2n*x>zc{K>^*DeJ)iuiJ=M9s4`QF7GoNEAzossC zty8DYQoe)s|8(rwP^v4j)XyBfKE@a`pZ2e!Kj*V=Tt)jA)V;c{hW5YC^(OdOL;H^= zuNwR??f)t5f8Tvc*2*+nSNz`aH>&R7xs>+zbtQ(Mru`2kKU8&ub4I&%nhRf}?3J`% zeNf82=%=*5l=km8`xI%-W?yB&Sqhvrk$-($eG>Xc#;Mx4Ota3ik=}G+Bvt`@j=ra}Ka_p(%juEt*zM~3g?yK7 zwd{#y8~uork3Q750m?_Z7o2>N?}^GMn6a(a{|Ct@-#F`?eD~{Jbx^1N-{+J|`X`*S z^q)Gbop+VZ`>9mwBwZEXyUU==`B7!8)4xdD8j&YFtas!-s_VnAUCX(j<$*s@j@CP? z4DvTQ@1%K?XA55Ps;=KV|ET|7%6h&-e4!mbRk2g^q4SND(TE&Jw&JoMT<#j6Gun{|5|4PAFFJ7D2z!l~i$Ijf(xzgBhjggIbbG;`# z58iWE`jt;>lwTBicI@D*-G3Y}Tlg?AOQyAzc4}On z_>6j1P|xzHdLE#j6_l-Ss;4sY?9}7f+cz=Jm;d>HioN|c_;5w+?en$X_Y0j~uXutf zq0YlGYZMo!vBIb!Hf7Pqp*CF-{pw(Cvv62+gueX9d8h3k@T~dXNW8`OqrR^pzhdl2 z-W%Fnc7_kfX6R@ZYjrDYx#Ywl?kcys;z#4dTkZ{wV%=D5_AhN?uQHu@#7CIN8`#%? z%XYj|&iE;a^|WQ0slQ`i{EXs0Z{5PV!4P?9kRbrGxnG_>C2hjSt=Vb^F}pI|q%cuK3M= z;N0Z6Py;%L&X1>p`{L1x(HF$pevR&)n2MSQ4aonsZfFi;4a)0yGMAH}>uK4CD?ERlbH@Y{Yy`XI(_`Y>y zfb+5cFy6(ODZOG~DK?V$$PW@PwBmm^Z!za92DqhnlROjK=?w5BLwYz{zIkjDxL4r3 z^M8)!7JK#v=4J+W$s!84=kndfK0>~F+D}NAsQpaymfZPv4C(w+s#|q=DLaR=JGA$k zi+_FU{^0Qj@_UhA>TGuyuH;dij~p8$#@a>6H^e>Bgil}FCq@GghnI5U9htCa@zGCH&qMIeUDUIZ zdS0U*@yUw6{Zr~$Nm=5PRgd^&)w7a%YN$v5?zDJ;+kQL|oo+eTTvwl>oEkI2YkG?= zZRmPTd95?=kvHr79Gy2G9I|nNCs=*Zb7k{!=~-P~V*hZtvpEAMJK__L?$>$0+B4gC zE8~~PHH+t$(Jkk4<>4chfzDTYaZeiYfwg<1hdDKf5&?V6X2sJ z-B;eP5BGRzwr~`Fq-`R7T)L$48_U6m>Cl|`&crDpbGgNP;r}vN=fuq6UA$Ckd*bCN zRi+u4Gc35H@$-*wqIb@<+Y|lWq+gwVu(j8xJ#O^oF;@H39&GOD56uVRaW{^e#wf|r zQdyf?S)0T=Y2IYgM#;Z)2AwC?yN|VLjX^uL$KDAHPF?aXPUEuo=iI5+_M^4s2hsjT zlD%dOIImxPcKp=ow-^g0D>adiNym_ZjBa|sSSY<~Cg)~kah93pPCRGDWcD%oO-$PZ zjG6Pw(OF4998XN{Hg{&0W&1VCI8YiWh!0n!o(kj3H^=lO$R-QkG~Seb7P5lsS*iE7 za*jLK8lC^)q723LPUC-w{2}s&$eSy>F4Au#z2xgH#8zm5{%qy!57x@n1<32n+=4Q* zH*$AiKhu|7XVw$X_#Q9v2AAcF+JZ0YR(w%Y#0Q`UZbGLLa{uEg?H!K5PqdFqnO`UK=%~+BdP6czFnW0ev4(7l`cT zy3_nK|CFYLv{ssAd+*lBe&hxIi5F_d&vh|=uJ?l%j-TsB$Io>$ey#%zVibx#Pedn; zZo9&!(T&;w4M!0~!EctWS^NInevvZtuiZ4E*JKwV6be=yV*2b(a z1=lK$*Ext@PR9(Z=06cc7WRYX*Pt?R}=T(g-0d zUt3~ku26onZUr*xtFd2*ocY6EN9)fV@%!v!6el^A`KZ_@6O4%+b(*)*%d);|&x4-$ znBfh6S9`<5#$8wKhBiUx!s2PIc+u2ht*v}JyPl)SWTJh$MdKQnd);(lf6}p;yvJkz zl7Js7WBW`l^M59nn>ii>EfIZPBD(o1XIHi-9!rCEW&oq$HHZnJcyDQ>Z>9apZ;(#( zUo=8==oZa?(&;_g5A+PUq_aU!mUE+P;ZS5PIi2Q0oztK*f%dw3XoN!_48cqsFzspE zs8{r%6*|%S+={^kQFNgp!PqGsQg7h#lHQ^N_^Zp0a-)3LjZrXN z2AuUhzLRIuwxQsxL(^t`22HD_UFV`{s@tJyZO>H>j;3o4O;Z~jy4D{UrSB458+sO9 zOT*8^p=%CJ+f5s>16!?n(q1koQ#-{QX`UK&1!bS4ZE>L!Q8C-K$Fb;}`{pz08}#l! zMBgs7>Dy@LnP}Il3nH{@cocn$qi>7bl6Sr=L*5?~*HnN}F zs`%)eqv#v4p5!ex9ek!ed7W=6)XbQBa_AFYL_gBz5))C83P-p&$)`?HB=^QaeMK2%5syp?9_5o#adMDG%S>0Go7wVZkr^Q=dO`9{22Y5<;vPgP37vn6t zE;|2Z^+(B8>9*8}XXJJ2J?|F#uJ#M2yt!w+&$r+67v>R5)J`+hR9sQpzd}9`qH&Vh zIDN79_t+4!&lYSiu+NJgAFUl)L!5Kj|DRevzVlbCA9sJw^}~{T^x#_e`pud=8NKAE z*=GEGZTb7!g8ZM@UVo1_qrJTMlx&KKYiE{MiEnPf22^q)WIz2{>X#2Tv6GCpp^K(n zT3c=vJ%`2?-ts{4-Xwc%@gT2dJ!!LO*`F<5xNQ&V6fYnCq<`V=?-lRu*a9yc-{xfx zSNPROiitsEy|YK0<~0q@2=^3?Ag%b!w6eTTtsU|!h+dzr=fCF_E5=sa9`t%v%wg6C z$)5dA-X(V4TgcmjZ$KI}SY;0VUS4O5+gPLZMe7K%%ucN>v$46d>Rg^De{%TaA!~-- zJ?FGT>;0S&Z5lIu%P7OODvz_zjrZMa@;ZllX8IIMMzI_fCoP|M{fl|jP80h>`|iZF ziGBR6_l;-0KbhC*q?LXlhWI%7(g{#Uh3`coP2h}9E1BP!W9zrZHlFpVZTX|c`)=5}XyV;Qzk;&VsX1nXwa-bg_BlLT zW9#>Qoj#Axf2cU?U#+n$wZ`_@`-{Wx+wZ&C_c&uajI^!hsj&&9ANAF(jD14sEXr~A zKiV^S{MOi(E@w=7#uj^4{>@`=qc_>cH>%9|(mBY$=3e_~ zv6q->mV8ZPTF02)IOiAIXGtdIH^%uq!H0@XgT2w8URb2P((VI~6*s?N$5R=K@3j*r zMX@8d=B+4JjJ2(|zOX2Uw9Qi?synIt}ZL&C0>*S~p=lIx z7HH98#%bO&D+VW+wcjYjE+3uaHzp+;{T7?OOWTkut4uFG37K^p27B>K$iN04T~M;8 z%$({mN5yNrq`!0Ek?(|bTpsi_bJDz{q4(#s)z|)g_F23_w9QWgx&_I=n-`xe-nwo3 zvGhFj#@K7v@z(RcSiJSfd7itWfzXOKBqua(L|%7~U3UM$2Iun_2cCOf@zxi2$6G%_ z|IZQL(9ZR5V=LpxJIwQo9eIarg{(4f=qc}4hwT{b(n0?cVO0ZmX|&CbzrGkb33?Q# zZqWmk@4PR0+dkvOiPNWkapI;oGVU4Z0-Df&xUIP9b_}-@gOF_?Lv+r*^wUnqPG5Qi z(y#gYA!4SdL}I4nV{n-G=}p8=M~1cVB5-&sx)H@P-}YA5W3rR_OL5c7k$KAZE0U(e z#k+HcrS$cJ50Y%1C8qP_boQy@<<12`Bu-_lFqk3(Vx+}MXiHp zoquG-ZH+qrs7>TG)c)41J+D*xD<|y(tSNb<(K#uKpQba4^sj<)hItf& z9GwV$-+38|J+4?OGx4dm{kx^d)&8c$+T--Z>ubn2)@1!rc$GWf6FIZTiIcukG=Vtj zYR4?<&>ksfZr!KG0avkL%E0`KugKK&E( zqCIXSUe4=`-Dy8hV9cy|c#KQ0Jw4y781i-Y`{#-wzdx^2yhC&hdBL#iZw$kH>W}6@ zv^PHc3?=B^G%H5lesiJp*wOLAwXQOkb{edmKV{xT$3f47hmh=Y-n~C8-s`e?gNgB+ zMaJ3F9@a?pd*+Put=Qg*nXU79obebsAiq=p{ouRVCtp0@8?njxog>cA?=+a3;tds- zz{MPqEVL(nHhda$(UAvOJ_E#&UJ7i2MR`@06{9;+u8X>y@^yF0?nPanRsJ268y$E3 zCFrN>Y5h5Jp8sc|>i}+SR%m6FR zh4dLT?{dC1z_)$Rq8B}}%|-JIPIjTbxBEu@?uxbhWSBR<)7@#~xoDLWi@rOiyP4m4 zMl5>hXD0PHu&G|f0T<1ePuCaie_}FPbJU~_e#-trFoGk(g_yHo)PCX9V!t~w4e>R% zV(a|D)L)>dH$$gWWMj&?8k>zZUHJnJG`h_i`2jXzv%Lv>&8>Gv;)~yT0rq~s8-E!4 z>CF}E`Tmc*|7g7QB3thqGA^|FUdzT)XPYZlz;yiD6f2;*;)mm(dirkc;Ck~dp+i2! zE090W$Nr-bpNQ$mrCX6n=a{|QQu*I9(z4l^7i1k34>J`1s7=^A9)U*)Z@DWps#Er- zy-N+suEaLHh_Wj2E0I6RC@%QBWAZ1Nix2i*Y{aL+yS2dE&vkhdau}=4)GxiV>{HZE z?1QX2Exr&Nr_ssFzY(2&DCs}MHta&;Gsr$%Heu2K)A1wB#4e-}TTS#RZO!hKBH2VP z!>%>_^go3r;j25pbMLwZ*vrUP^J%VKz$icOqtJ>Y_#ix;ZHuR5&pY23-MToLv(f7;d)9ZcRdw>#sJ`b$Q^%q4(B~uTz_Mq(-PNb)E7+vo!SxV4^kP@v(syZJBDVUs z|7_*p_h{pEwH15R*ODI|{4lsuh|jV7P2?}~F8sL4zL;1IHFX=R@RNCN8~s&7nw96H zmqtfXita*wm2LRHhm%)S!P7reI;OH}UhLj=rTCr{nwP8yKYbN2{p)zOdsD@u*rYx( zK78j_$8EdwhH+|h_|5^~>aWLZ&u|@i-gReWm8R{kD&$I~j+)`}ujvymO0KMGO`9H|Vq9Jif;7Dp-dt=IyK-5!V{U6Zq07EiYAq!;(iZg{6QFr9oy!4D_jQR?aX*lZ^*NP9z9 zZQyJCUj>fr0!Q|NBO8G2I&g%#D_h&l_U89Ucd|Bc9qHz{dKX;>jx6Bvf+wmQ+p*FR z?cC2b=V!}^)nMa_2VBX-cDXTbNuZB~FT1OV$xwQfF>3)|R1b0R53U4XZU9>`` zcUO797mcOHMfkD|TxrEeioM-4g=1D!6~yjdSIF3TuvKau=xT3H;aYq>=S0y5?j?bi zoa-ZY;z{Uu>jJ63J&E+HHyT$~4&QI#M?nNv&_{<`e@}bgq`mLe29n8V>1XM0<&Va- zmEf6RoC9tYBrmgYZ7aCadf04NInD1-#s}aDH1y*gDe6AEzmSG>6LrS3$Q!Koa<=_p_tt{}d4|pQ~0Bmw?`}y_Y%m&)^dTn4Hee)`~DEs&g<^an+ z-daz=!$$YOn&z&e?3P_c_BiZH$Zq&~ZQy3|I5zdlW8vXuv%Pf-Wge;x+`#|a&HhCj zBD88T_#s-ASvR1vc^!2>M!J2q0r3qh@EPj^4qn1F2|V23B9=x32Uq-Tg@uEuT0j1a z?dMx|fiFKd+Z*3x%qj*m|E^>HT^E^ueX<*V!&(Bow!gp_dTMYhXLhGFT%fY|qO0!+% zv^-21zXJap`~jc-kO;0I6OcW-=Iw8Jw{Q;|rJMTM_%vDg^g_9Xn=Rk!!KDKDujbOf z9+y@n8`ItbZ=1kb;Z)6#2u|hG-fQW<&9wnH{p6&1s}dPRN^X10z}$AlFzDh;4f(&a zE_ZAifjn?2GMI$i_SVGQ_Co&WVBaY|rG7a3`HJ#@_i$6#Qr<)d+2fWPD zwo%hVqA%0I6WJ;5NyUaW)fm&{&aG*vOsHwixhA{0>+0;LirzJa&@b^vtvS82+eY0O zYVL~7ZXWrKP(x)=jq2z8kb{C#l!;Hb!(XFKPY12`>iceD)HDJMK4;q>*eLoQTXX-ACnmRzDhf4^oD@2{-b&KV zc(N{_x^v;V#BUvDtObu~Qq#jRHJTHxyYk>6@*+HhE4yJNJO=59kzRfnZ!LA#G~;90 zLj0%<*4$>|M>Qto(EK_slb?S-kK~s>-~Ry*L|YtOaA-_C`s(iTmnH+_L|`n4!05?te7H8S2wuFY!dv6z z$k=B5gS9rL4FK2ZYw>iu`-8_DkR$OthiB1c_^j>u>fa*zKfO3KgZNeQsrJxs8Su93 zHMaZdSM{CxNpnX0iDFtcR`#mVTx#Bx&%DpiZoIZUp!`<9Ep}(Oq)!PojU|RtRC&?m zSpJL^aW#td)SNSc_T)#}~+ zNUwQzH}Ar6jbl0UnmyZ^&X4oh$0^2IF81ud+$wqN=UzKTF%Fk94v*Caw$rXJympL^ ztQ})1V|{I417+x(QJsr<;-+4qle6MN?WLa3$9LjG5IK7^9@*iC?zr$r?q1;zcWn5S zJ0`pVS!)Ozx@N9M_M{CXjrLmRO&fC>eU4>!)QbJMhj~uyxwdN7c*%cL(fj4<**z%S z?DmGe*tIE7JMmwn2Wm<++RNcZ=Hk=t4On(d;kjO8$Hc((hdjA^*NNAT=hFNVuU!D2 z9b(@&7M)=;`^J-r#*57^PlEROZpx9|SaN6&Wl85K*)??6is3>T3*~n_5WnNI^X7?v z9&NmMDfxG^kEHyBv(vcQOMc)USkypU8r_4#P4Iv<2fH3Sg{&E*BF@HneD804-@^Bg z_^z`%Pg1VVijsZ4_Q`L$655R9N#B+K3;XKQ7wqplsm zUTV0%+F=k=EY*D;=XRV&zp*Bs=$f{Td8zgY?q>R-2d+WH{TLknDBWlu)5my3XNZ1$ z27Kr7zkm1={z*=5#|YtkPxl33cMA5n;Oc3l$=5`CRi69<(o|k$e&{6mKaRAo z(S1Ju2h#qO@cCzyd4bg*@4|;PxrdNuK=_q-Y6 zSI2E;=mYAFr98WTCxp}n!JxX@xqrmH3Aj%8#x6RdlTaVHbCdC9xFGySRJ~iNSM}vm zpW57TL9ksiaOh0N)?a2?jA$y?j?Wliay$k=e9eK-uNyO&!&oFW*XC zit*;*+E=%5Z9Dy{e#ejRU=y&lF>fcrH?+EP52n;FWR55X+|pufcf(o4fGcrtU=PHa z2oKxpc89*r`*^-@rjJk3Z>Q+5C6w7<;rU?7J}>;EGx~pEr2hwn@1XxPpce_mn#&p& zeAa7t!Y6@SaHw9vHP>8U)#_SNwb*U6b-6qp9^%!h?0>jyMV0CqbouhC2Pj|uHZAUa zcqH^j*4PU#2Zz&u``$C^?;olEyzuqZFFOFiV2`u6E!7;@F$=!cMLlbnlN$3Marc-* ztoVb!INtUA)-Mx>^LW>o7V6UYi*G~UK5aMS7a7+!lXv|C=C*3+pvKbqSN?w2G>vf+ z`E4Ck-|)sS1)nVjPUk#dz3X2SID8nKE#jYrm)3X*&-;ft$Ekl?V(`Uy_#Wv^a~a<@ z%8cmM+C=9RccjFe*iizW>3bWvBHYrqT6n8w_n54?jCHH~sw{8F?_ZE6kGsZc7IW98ZGFQ{(3GisAIyc(pfxU{5&4X118r%f zEzrgAz({*++R~rz1Hwn(O*9r>;t&ca;=xhjcT+kzOWj$-pHy9{b1vnJUal~~4b#9^ z&B2X<;Vf{&174`E*3GWaY4$gQp%J&s)+*QBW{q%6L+gx7d0e%;-*);cpZ~|%7F3mgj?OfXO$@Ckl8Y}WxXA4M}lI(!X$0cS1N+kS4+OMfuc_DyRZ*=xT1)2^;9@y+I{#>csl{oWIEW}{EI z^~cuvPS|E*TQF@oezv>uyYR4HPQ)h^!Me3ZE{m*>`J|DoOn#%HZ(2M0FR|A`Cyk3V zisz@cUy0AC)l)dmDVxiVkZJ;7-Z<>H&0s+B@(popLenit8XB88>O0p}F#9 z>Q7x)!w(;>G-`sSmU9bCdyN~ zTx9FYE8Cbf{7utH=jZ>Gq&2x4X>)udnG1ewh~5GJGw{oOo4nHdD!zs6nBw4dtNpk5 ze#IV5-?q|^@$|_%>w@etb2&r$Dlc$cs7 zPgD=KDIxyJ9%naq`5mh~r|f)USz6_C*Pd?XkXsj24{2PGi{8&oY-!oiE5>vhdRT*Y z=ApCApx;a`=~EcHej8k#(mM2FR=aBP8x}0Yp_I=$aHc-vclEZBqtg0+=* z!7Dgz*t~58z$y3yv+yL3=L*3#36ZXS8feEA7}FqQa(PHc^H;=Ui;_xoD0UFpI9Vc`D= zpZxNsl zh5y6B|8(%*5B{Ss9F*yEQUxUc`%9JRX_%D!ti z{qMX>mb9fC??+fRPj2jfnn|OwoN|W}I~2Sx^>f#rS~yib1h=rE;u3G};Qf+ry!V6mzyTguaDn61;7atj3=8imTX@gd?-5_e91FtpBJC@oHAo!HS~Os;zk{Qz^%6Ho&Dk?9^SPkNG_;#M(aX)Zj&#DOKZVQ z?pJdu{acB|9cNA~0Vg)G?#Z@pGtY0c-pJ-#ZO!AF#U=YL^?~|g7MGuSG(+*MKS3LdgjH%{c^t{_0Yh0yq z_YlJ%^=@Nf0eJ6;df&~Mstn;aGOyJtGdK6TGL-D$_!ZKwnO60ZB1pBuS0 zGv~^=J_~-Gjpyg=!CVJ^)g5V@b?z;=Aelz@{FZEoHfnw^q5NvDi^ywom4oL;#um(P zz4Y4oXUz3{o3C?p{3hjF^ocQ5tYFvO)4z+YG;w-uzeD}ohK@kCoLSka{Vob5OwCCi zum~OcHwtsHnM}(niqB2%Hy6E5xokS!JvN;eBLi_@wQVJ5EQt1xaBMtBK?6Rojpr)S zjZEVcHl9Q2mn`6vjc0E&=?d9+3a6s&JS%1A88DJ9J5PKLWgF0K=UHBE+j$N*Mi=x! zc0PRf8MdCXDHV?S8Q17GoMH2M!+E36w)y;E>Q3zmu$wSL_z8D*&cb%0zVCs?(a5ji zL7Ng|YZ^wD2OjbwM^${iimPiX^(;B|NO;#vYXeL5j-Ib!H1DHo0~LBl=Gy4auZd-E z(~w?H%&o`a#q(;OJ5&~W0=aiAGQZ{cYj?R*y7d}O=rx|i4kNVf-ZSKk>3mmRkAUyh z*lUC~+c7^SpH{3+%>(3+t3A0FtO*l;tuf{5nxQpyfy2Nf9!vMl*ne!>_T7;FIeG)h zjff5Q{Qb|?p=)15Os}=*+KbV(PaU@zTaV2Z-ydIu{C^`h8`8sXyR+Z8ZQJe+J%uj5 z8oPn$auSGlFrB^FJoaL)xviA*wQAMf8!!ER&7IT6h3`CXT=?m`LYpi8 zW&Bfob7G9xHXI2ZPu!DVjmYxF(<_f;D)IDJ4*fOJ>aUjZP3ych z>Z|6QtC1Z~Kziq?s>E{@4opEX0CVYPjikpyLs_M+n0K=H@neYOd7LB`ivR>j4ow8^7LI3v)_($*I4Oy zd9xdkDO>X-h5H8ncSZ7d@ekd>caXuLD_v;mL{gEXv!C*q!F3+?1q&ko8l%1|=8rLM zSzoJ6*3Da-{K(*=(<%Qqo&1mG)i~uk`Td*?Y;4`WK(ussym3{-(h1gBG(60hyZh8M zUy$40l8UYonSR@@YqF#JUSk0t*wMPHm)-xR=wKecx<>oR!|4`<9lbzn%(YDJ@ z-*^w{w67yI*nW(1o86aMGCj%h z6bG*vxp0V>r7ef^LXtx%&d*@V`T*G+XH!^mDO)DBwAc%cx*plCdtV@?lPI&*JvwW8cJR3JXg!zCN51PD%!GNR$PSMncf2!s zUDZb^->=$0x;A9dl5?km^NG;oX41&65IswHE6>x--tz_|KU#Gv|khinS$pwYNA|8JxGVLvm8d7U7HA40JD& z(SH=xzCl*|EfRGDGQ75+gVYUh<5MmWAV|ExeSx)X{|u2>+6QJu*ymG?sp2NcacvA(E%6 zEslKswB)Jaoz@{QGD_iJrde6F!X1&Lx*a*{U`xhs%TWi0o0(f->Nx>!o;(8v>0465 zoB8M9Z3AUu4-~$O`p=f3rkX>nGbtoLwf4mk`Kdilj_i~(@_=n9=gypFUNtbEq_63` zrwIB`2%VS+y&#UwP1hnD!av6Cw&GO54_(zxy&t-h!cU|7Y8j<*zS5cl4t_TiW49Td zjPP0VZpp7&kPB;0`OSik2Iyx?oGn8aZV3lkpjrB7@Gk>AbLLwMJ}OSWM$YBYeAC!F z=PJqGZ8~#Ucs$+o&TEOni5zfV|5MQq2nTx7jLvfKz)ABaX@oyLX~tv;cbxg-q)U~( z40X|_c{fDnitOl}eZ^(Sm#Khn{#K>v#Seuus8OyBo?J0_R- zb}Y!{`k8O$f|nM|^p*H$`Ox!C(>xZ>rnxv#WmBi>&Z2zDAL%oz-g*4%NBLSq^{o+E z(p%IU%eOiFlU(wFh%EB9zPI4RS>k&T-Flwyi3Nv;Jh9*>LrNA5AM)0M+S^NflKZCJ zdy5sT2slfZRPakJYu62D0X9K*Og9N1h?8D*q3q%?kz)R zSa9o`V1HuBEZQ@3LF4Vj74XmWy>z?XpR?|r?c=Pdft%^)aMYM<9iG6RW_V2R1+%$+ z;;EkeK#XVdlj9zo{F8qlw&0gBLngl*(|hu-em!jRFMs{ye<76w^{fw z!WV}uEQ!l|H~Vqq1B`E}jSnMyNg8*)>znN8`?0^vgr`d8`&8evll9N=Ju93jeW&;3 zmi%Khc+fcH*#$Ef3KoQb;ek@ zFv9l}_ze&Lnua_n*>q`A-(lc`AABfTFl@-%3*5dZ=}$j+kOm$M0}uSZTaX8+T_rr{ z@_hfe;oyNEJQ&IUH1Hq|JU~vz6^#e){KoyQS58$=e&+)eIr$a%$=$%W_0Mrv zIWVbi!L)@-Ff6sk)BYwH9<;`ibAONv%?7@gZnyhn7O?%6aZIQ0UW^*gsqAYdhB~FQxm2=Qa60B3%sk-*A6o9P+WOt9%de|4FXz z^X&z$+M!qZ8g3oy`xSEGIVc_#Kkhtpa@lY4kS}I42FQz?G05WIK#c+4_041q^i6V^ z^BIdl+#9LS`Cr@D@4G7X#mP&2?F+8tUNUsFwQoN!_09$ThkR$jzW}EnSe9_#1Wp8p zUg7J7d`#zzi9RS+Z9iZsqz(^oOa&&pY#R@S6QWDbw=`r*bNSYbZ_YTx(MHLp?f@>y zrZR#38OB6-c}?FDz6SqD-x_4iF7V?O{$BDhG1>8oDAFp_Hu*LM2oSuVAE zH9DxZ^pWuS!66G5B+*C3TI3Wixpy$S*nJGs0`| zz@56tdVi32?Gu*pU;Pw{!gt{!`@=gvm(T%NLm1bl!~ z4CXMr#3C>D!Rk*p|0EZ->t?=*!vFY>(2{MkLl5UbH+-tV6K3F@GSm% zf~^M^|8J)q1l&_euX0uX|L%+%`vHBBg6z&IxAPusYG&oZ^Yqw=$v!~7tBM0EKJM%^ zA6RFSN>}U(owz{qF4}Pzzkk+*ZXY~0%J!M;C z9Vy$!p`NnQ2SxBFKMH^HdZ$N~Jv35w&ly!+ZpHaeWK5D6o8(aYI`wfse6eEINM?6{ zxEa-4;pgtbkECjRXxDc`tMCt5&G{+sa&I$x@7-!9C(xGCIoN5m@qP38#kEtwEd)T%vC7o+Pg5Qsvy&)M}kdjxH_kl|Obwm^+}-k*E! zJDG%l(t7swJm-)5H8c0z_jP@*@Adut{a)AQGUE60{S312sVCcxtTyl8Gqb0*|M2ha zJ=gfg{`QSMOZscS@juVq#3%l6_^EBD&;7z}hB-4cR_Zc)S?l62Tz9y`^tUH|Iue6REx<8a|eEBoJd2f{_`w|D&8b$93yo z==c}onyyKF|HWNfgEQCvprhS2rR$e`Z#2xVpEdkl`=4*$iub=x}rj_dh# zPj|e__3*mqI{wa>()A1Oy-&Lv3|Chv_kPCr?=-yDe#~%pJ-p%XtUkA8ZKKagU6a@& z_&vMdfBV7u{)gvoOq6eMbp?L))&9JT2igV0f)BphztS)gMIK|Y)U|dG{&{%jx@S9z zfa90$Yr+OF{t^8=3=F?^=Y>DZ{W}@|FW{{*?#<)BqIEBIJZxOw_2=tf?)Vk({J^-O z>wWHD;QKFlMmYI3{|Q#%V!^tXJA{WKl&i{O5JMI*j$kw0G-v zbrhPDr~RC>6R%z0*^x5+)6PH#E1B~Ro*#6)N1tcB+dCd=c(uKEc3|4+2BRxdwzz+e zk((Ic+|yK}ztwN}-`*RD)V83r`>3ltgFM@7=EQfIx&Ain!qa|Z?tRz+TIY7?2F_P` zgZ*}~QHE!v&usV>@SGPa*FwmBa?)PM7aivGNhD*Ai4_pUo9+|K8{&`&A%Zh>~^)a6c_$?t!t z>+E+*K3GChgTXYOJx=YPLPbs%#-fB%tOBR6@5 zxOOvf=#W8;?PTLc^y*v2{+{{8Jo5zinUmxb+#lv#m!rf|r*l6}U*(g1DfNNhVy&}O zALLUKCuXG2dQd&B^PHYJ$=O#YZeec@o(n@$T3Z)l%~cH8qgs2#xdA6xqt}M4Qyjes zxT{zTb7DsNoDS{bpT-R}pTu{EezO$0N)6@1OOUho{hU!YfG*Ago)GO1&zRI%PRvASvTJR_+vvMAoRd2hyVm{@lGSaj zZ*4m={NgNNsO75X9jz6emERUD1%@^YhV)rmjldv$I53i*E-&upVu)sl+$s>o!fi5uCJqS=y3b^`+aq9{!aJL+U=*?d8_??@yKP;u%aWl_8G)`x*6p_BuXMz&tX&=7X*k?|7TGF! zo9CL;B|RY-DjhN080ZS17xG*MKlY$Ea?vAejhsZD&lq&mXP=py_!rLY&0XK!@%NUz z$qDDJf3D*{Ajkh?U3Xenp)2!T*LD3Tet!p;PVrf~{)d*nJY%}MBtyU9x~6Mi-Hwja zeE(a<``5J9H*a&|IpF;{_hxU%?&@p&+xF*-YZAvzPuH1-x7xc+_m6Lb|2$@H*BPF{ zF6hd+^T|X(=h(I->XA=f*jz=_G|ImGD)gVl=j<}c z08dKwn9$P2>G*txxnK|aA*nTdz9Cn~LFD%-tJ;6rPUPxHIn%t=Lzg%=}?DU0=Y3yC8xM3x?;248I! z(Dq*Xej2%*1K(@y-Wg<}$36$Jn=|#a2KhAhS$1HPwf0Z4rBd>k{zv<5OcWA(*B(&s zFMG|(lbp8sbus6S-~VAnzl9Ivn8sRF_+}#O=7Wi|;By#Rd&bQ1J7@KsVU7!2#$(e1 z8xqan^kiUjkTnV;@xW~2R=09>awfpU>ZCf ztzvh_BkGuxg`+ugLemo}fF?|tLi)ca~j?|Ei6B|L}yt(ouV5ThK1 zc8?Kr#I8ye5vTTGD`h^T`z!H@AK@H_*%yXiI1MbP$rG)GjzzP5KQ#k!=(rF2OZ2O_ zz1MDIFF4XZdWHCFPHar{@dWR7)6N;*J+B)pXaPS=j1$D28O z<>nhZsUbYT1wBR&e%Z(`9&$sEZs^g>@8b6iYJnDWZ?G_Xf4BLN&e8I-w^;rz=)=SBC;2}YnmEj-=ZoDP*$ZxM?XH;9>SoN4c^!ZBGV>s`5dG5q7 zhhM0ok4`>w=+}$P5>E=xbEcAma(QF$&_!^tiFK8M|thzn)oujF8}=Mp<~KegTch3;3e*SB+Z{Xt#luJU!Ap^K09 z-}W~v`v-^xoiqanPW=Sk%l?T4rWtQS_s+uax|djUME49c{vLW*KE`3w# zT#xQ~A3t>08-49R$M^2${@iuX6NC9d#|P*^`LjR3hZW5t8~q#Wp6j@i-+Ak{cl>}D z!**geipM-(_q-LG__z4EicKuwnPbEweu(Y*0l$lgsr*&lUv)fx=XZ%mxVjG3?daR%AsS+1antqV0nTT>6`NM$ zIPGe9sc)#4J!B5GcMlzGZ^OS7Kb6BrZOFthd;l*za4Y*GXr1$R_x$en&{^Bvf7)rB zCx3~bU0}E`NkvGX>8mg2|EK@p+ntIr5aT7rKpW7reD%|| zufBw5a_>(2)Ni6ktMF4pKGxk>K7ZP;t>hW$))S^TrZa@7%OIwa_NgoMAAb8{eDXIv z#)~1tNQOMl`)U9DZk~CLF(2bBp@y0J6R+`o@eM})qu4N|(|Z!naIYMDu%fUh@io5h z=lWZ8@U1Q*`4rC#xWi$cE3lF)@8$!EFjtA;O1AO)>v@|KgHtvpXb=zx@6GA-EBJ?Z-Axa&?(G2X>CL&wKeT-R0Ql(p|ELHe;7R!Wi1Hr^PSCp2BC$)qBLBXR&iq z$c#2*LMy-7PsA_1Wyr2JcVj2#h370NX>DzcwVpKdTA!+0+5aeZvfs7vobRKPwG`?&r>!6&whC$eFQ1%_MDT_?@Rktp#H&X}zA!^73H7d{_k4&% zDe>BPyE@ylakEoJ_-3bnGyJ07an`}tOYd>}Eo#TA{|i{9(CY*Epqbe{B`YGhWJ z{#59@*y8BDC_Wf;*($p_(^h^5{nrBwyU!25INpw)a~?gybqf4s!}n{T3GsNhWj`NE zbi<$8gGxC~%~2oE^|78*dqv5{ev|)oZhSZP5^>hR6Ju!M<{M?#?Z+-oazP8xo!D`i zws9}N-Qd8njh_LBmTinZOfC2Aq65t}@u=H1@1l>BruVUdy4ES1SsxQMpFscPKMt8o zwz7ujl=SH*uv<6s+&*)xjk%wFyQ|rAx0Sgp0S?)m(Fe1_>G{0*#=ggmeC9Krp3kxG zW?S}Wc3ktB^WpG3XFj#Z+E&)AG%=sEi+Y1GE^L?}whVgNs71Df(>9DhtaC5R$$?gV zwD%PIa?|S{Z!0sOan0wO=ZEKc#@V=w&kxVNl}olQIlc#&&*k0NraG6DeX4?@ zG0l}TpYQP8)4v>kfcYG;ftgQYFX{RG{zUUhjenAB=S=3Z&R7!TVhu#;4(4}d=&PUi zP{-fPe+%f}@)^kWo+-2a1$cd*`I#~Pf;HdO)0pq;Z`XL^?5B{P^T6$Gp=tT8b^d8< z>++TzG_QZGrOvl(HS2vu7l-QFrp%gea0ck+;8xaPbb}L5;QBFbN14;D%x6u}fuQ)a znc8~AN942h0n_Mwj)f^3n1{jhcII>K&@jjQ8rPM8JN8(M)z4?o1$@$zZhveMu(q)V zpnGO-5IGQpUxOjR&u`A+e5}RHw&oXp4YOt-e??KG7Jdz8=#k6L@9qo3^N4vrYc$@+ zIdbWB6pMew{BjMOc_Z^{@#}2XBQU?+>^q&wuZ*vDI{PYE^UE`D@_*+1KAkbYtEe?q z9iIC7kqbTYy3RkQwsoWHsqNQ(paN0G6&x^j{Q?0>a3usLxC9ovWAYol*`hArL1G<@FkFZG&PT?1xz z;z0JcAH^p3wjY!nrhnF|2B4KB^LL%_a0WZB+|0G?u2tAwr)=AewN}>&C+tsnaU(dX z3s-(*7L4^ z>X{#*9!&Ao6{U;LJw!XJ8N(v370j1%6+79}at&i>D>as!W}ippD5RIQ|JYXM-2>mA zW)8LgXHB3bks`0?pQu-L$Pblm7~k&sux5+8f!jXz1SlXm)S=)&TVJ zy{*A)?O|rwg8r^f;;`hGd^&fY&n|r)za?#VX-#1pxM?yyG40E#z5Svy_a)+7ZR{IZ z0Bpy};b=XF;<;7WuP1E#O?jm^!BH+adV*Y-*4R7i>38$&ZuaXEUUJQx`rvpP1Ki`u|K_Djxk5hEuCSc0vR?K*o-^^96yQq1Ku zvszb}*(o3QHo{-T8CtjUdj{X@;oBHK^z6o##42pOU0kcN>AJbd>CD~5p69#alTBPX zW)6JnJ#f;j+Vc>;O|{~)e6P9Y{7WsSzqRJtSD_R59Db>p98Jf^bsiXTNyVq7Ym3Ll z59_)AIx*eKareb5l7$}Z67hux{yz@hcl~B~-Y)w;*(}`|alwh;Jc?|82OA8Sh^gen znv3L@5LbnkR4+XXzIMhjaAEjIsv)ZZCIcNG8~Ts^z06gN{^CUjXNR*7p6QQWD|*7_ zwCHSHELL>Z4PIl=m>>JG5_&6u&Z?Q?Vr)Av_z7X#VF!$flPXT)z>)l5_(%VfIRitw%x6dGst~#=x-l#7*`=FI&{XZ`mTls)X)1csyyo^aO%Y?|J2Z6{H07_k_8;$T#eTAB zDs1Ik7;6@BOK@Y^Z^X04*a?g`o~Eg>_o1of9_*)F>?dd{sG3jmQw~jy*i6vWNZfGu z1aMk3g&mkdQ^Z2qTXkk%(59(ywPMAL1Dd+irm46^Q+2^*(9}F=YI%IBMN{Ox9GY^c zX==mcQ1>2u{#3z6b}+R#oqLMw<+iaKhRVVni>;FP195w4ruC5i>9z`g*Tg~ zqCRX+_N`kQAsz}%#SLoI9Q!1Lrhw5qCdMlq+cY&dLR~5}HP5E0dC-(EGgg~PQ(@@k zWc?HQve{-gJh?P#29}Dh)PHfAk?)7D_7;`Id-*Q>Dki$OXjXg&wkEP_l&-K@cX6%8 zHtpuBoNUa^xyG1_%^7v(EX*0|US!rubIR>2v0uxv_4d)`X}34lhn=^Xc_ts8-#l}F z@Ez>Ck-5X>?c$w*Nog94Uh}9$gHhJH0+VE0cs4R%7Ix(*4UV-rg&((N1B|^d8nomZ z_M2$XgAFIScHc!yu37)}@tC3jOi^tj3qOWYptUC8HKQ?tGG=|KoUB#TMW>YlA`W@eR`pUA$ zFI#{)k5+3vC@>WsyRWVQ+ju-J$!4}?)P(dT8^NJHWYq{={f6cv&zVlflS@@YKy3G-a=E970mXO*xu0N<*}K~Kub$`pJg54R*P)XF z!_>xn3#3j#-RVU4T~7!W&)zv=ch4WS zyHyM1*rl28F8pZtM={$jT?lMz(B+GWb8gKx26xETqfTj2MQ?i>c#_Sq*S0TZTOP#5 zY@7GV?Mvkx9sBYSbXrsv*`u?iGv`ixvbd{}(=| zaB&*jkGzm2The}|(W!c7`JBRu@Fbrz?T?zB$Eex+-_+-P(ma;-Idkx*Hsd3m#^+qc zTy~mS6ZxD^8b9x`_B;L8@;Q%DzxccQoI7rr-?`oW&-eVcKIbuV2J$&?1xLHU5ogf6 zSjawJLw{OHO?-O4kIl#t?!TZq`d#q{u*YnryNd67O#hSR=gX6!<1yyOo!KBoLi+5gZ_JjVB{@heB@X99mMO+RTr z2>P-7%IG-1(x#sgzcNifzT2(6bTZc6S$4feKfc>-`kBzDwCU#)`;->_;8SMM&t-i| z=;uzGek|KRb{U^CO+W55{oqrMp`TCSOODcyjk{kB-)Yg0?L*r1GvY&5*gmAzNEK$_ zEQ5Y5yMH2_rs-$29!K`SY5SEn{aE(@MD!znQodxS{}{VU|FIbV@if=R`HwrU!hbwq z`;XxX{Kr}1P1}D;)7obyq_umI50&ItDtP{KzT?^}_>KqRy-eS+1>e!49ou&-`~<$E zMMwCKmK?%&RQ^PA=oIqfllY9<^T6>HA4cr|p>MLc3W z-DIARc+72dpTedxjE4LZGb%c>&{x_YWiRzjZscpKB+_{w_v*2&-l2ZrYrwq**>?~h zV<*>1U{s!MRC%I$s0S1{KnfE(E#)x3M?)0o<{ z+-ZXx*_86_+K-R>FOJ?H9UJS_iDe2_A2?kBUUtw|2>Zd&*(Jg?`tV)g6WrkP1^OTG z%OB@?AD>Qtnegksg{-|x<2CJnjP}d=RAiOaFZzUjKGY*L@oxkp?$9OC>`cwtJF@c29SUG(Zo z_IqSc0`RT<0A!=lW@^Dx>@7jA!@=Pwj($73D+GT|TmN69yNUQ*@Q&y6858jNhH-qJ zMNRSwn+_d57fo1mI{~i~r}_lEp5_O^AbTkkoJ%4{A2JL|0MX- zUS8sR!Qt@zapq|}-!I^v_`Yf!-+#KWoIOGv>Xvj&(qOG#I{ujguLabQ>i0?upJ|!- ze(sIuhkj#p4$aUybL}H#hBtK&f6s$X@z%MPl~Z@I2Do(wVg$Y^XFa)rEmTgN*u);L zxjE8ViGDmj$#}60zVKcPetAcGA*^ymUTGpWPkrlhohuSHr`;Y37)uvUE#WFxY>GT{JkS(eb%Q|+->j8>kI_dKZHCA- zt6r<+2G++BFIz!>Ix{P&b%LBDSd`{_D?Vn~)ZpN2_l;sDIR%g~~&haAsk6 zeno$^IlHB@qPTT{u@^E{6MPN*Cer!-wF}P6rYK=uieQNcjKkEmB;z&lmsLANzPvR~ z?x~gg=&lz-C;pWB2VZZC)?2VlBXD?`7 zRTL?!F(VhU!He>?_c8B2E^3XE4;13OJF}TD(ZxbO?*l%~ zndVD#WX&1hHD^wJg60gmOFHxB%$e4bDn3sQuXS!<)#Uxwxq+c8%-Qe1LT%^0>_zt# zt>0(wx%XTnJh*e`@b`T1pzP&J_^|L4XF|bO+0dKLl-Y*;ULaniHe0xVh_%TMFRlt* zf#%;ZYZtJ`i9e@d=|XT9H$Aaa#PN03oATV&IfdY_-lF|1&ZZRYw{SM%{YUW4u4}s4 zm@7Dr!TaKG55L9VKH3wHi@&KAh@GU)S3E8r(%Ex9Y9hp^QQE2M@j}DDI06l}?)c&F zS?9mr$et16`=%K--#hIwADQh%t+_pDwR`zC@1f2AaS!=Xt4+xQY~y6I(0D;-_BgU2 z3O{B6Gqof^;UkJnh?~9U+(1oBKXn+Uy>8<^&Bz>kKfUh@DF{lzo0SA zm-X=ADfn_Ve7Ot0TszsA*Jkr&7JM21cKl`K73f;{lH@P)g)kymOO zeaSB5#bRJRLoVu3YBpkA)Zr&3qjIvzlg^KnJ;I!^Cd$f{O0IOl>#WyftqeHcAK9{y z&n#-RtC(|sBg?5N;d@)4IoQlxm%|G#bXpeeED|s94(m-~<7=DM+2P|3-BY7hp z(OIU{WCzv%X~y#e^Ps+|y^2fD9AF*<2lQ&yi^uJG7<;bm?3QZyD`oMQzcu;8&od8i z-!l=6#qjU|d)3XRUQg@fXR+>{{PD;cvT@)!3EaJb*5F?JKkA!eYs^i-UVKt;5v#{$ zj4~gUtm!ugwa<#d{+|){|BR&1Bg3wfO?L};*h!5-33WqTksT>`L+vd>eiw^o>{_YM z@tkCm@}D8{pUyeE;z8EBaQ10h7QrLQ6Eo5^QF**mjQ(N$O-ysBb5osnRX0`uzKWSQ z$uOOr+CPL_(x#p(He;UVPHqo(oanYvsuT{1-aTr%XU zb^AXXgGbTrUUYlGRq6IuC)Dj(=yvIKUv{Lm@`gxR%o}-pRfQ)UGb698YV;)i9(-H$ zk_S2#kNPK3gYdaX*~x3Dw}nTkql`+wRHE0V=M_)({8MC$VLtqQXWS=VVlSTw?9mg+ zPYeG=7XGDY40Fc!Gx2}#r;Y#WPaFS6e#%{F*;e(~Rn|O!d+;5U4HXSU$~<59rE7z} zjz4CyF449PKl48)#%ud0#B1X?y6&{;T5+Q^U0)urr#OeeS|gcZr~b~gJ@3=Stv{2t z&B)^(>b?ujygQ-kDN%2vY$r01nyrB7STazxHPCSy*QIG(XO3~!@zH#l$1p7)#H00k zT;)dE2MJ-v7TZ3E>{u`B>wVZdC4~nPSzNMnLbeYgd$Yxoed&Dx92?g0wZq1%Z~M7s zov-i5t{t2$n@Ku_E9WN88%K_-{!w;L7dk~TzcBaFxz^sd((SCju=Yc_#D<-ZO()$x z-tVw;{=*v${QUI(AR4RoIlb&yWqWF@=vyD-rM4(0-)>gner)aDgrCn4FqH#OGrF$E z8nfzs?>|zA43wTRKIpObh;hbzkh#&Ao6*}EzccRan`o19Ys~7S71=Y(8haLFPxm9+ zMmk;nsb*JWkD^mAwc*FwHYe=5Z|#-9_$|FL8j~=S#2_w@gP$6;ivKku7d3vb#%wM; zBHbsQF@8=LdOyAKh0K`7_EZQxsW~k^>}mCWBzsN1^B;fT>HFixQVi{SWj{S=)D|2@TeOP(1MZeGmOzL#@QlUBKK%LcWX$t9>Qo;bTMwBz9^x{W%+1JnW*fDN z(L|vsjVF}NT3l;P*DsxMnfYmhmMxxROj_^k&|l6?)UJ=v->h&K^tZ{PzwG#I{*zs& z^&|KImQB|M{i#l|6IpK2dC%gJ^&-{g)bv>AYph{km@}j_*5c5_<1}m@luQ}G9`YBa zYso@04=zCph!3`XA?eCVz7ts)aj;e?7G2@NkyRyW(Su zf0?7SkH@*j*mx&c!zsVO;T^Zb&qF=)C*q@(*zJ-dRm?>ew4;91Un_j$gMURovgJdp zm-ZLpyF%MrP2!2rcN~7Ig2sj40{9J_wN#fjCoV_tnSAHaiqrpzNn`s@LF*IrAB;ke zcK-p^ye+MQF0K9_vikSYe^7MkJ^Mp!Rj)s@PIkinZ94Tb=5m`ZL)5N1wCT{La!(F8X}r-WJc3z36l7r(kATv0}xYi8&|#589PaaC|a3Q~Y!9mzv)84Ar-X;UCepVp*c; zMew59S%JNv7%Xv-RuA>8TCc8nEiqh+)+_LF7SQHeU&AYFXjkxikZ1d7v(xk(=;aEV z?5$3o)rw6>j-e|?;u3aTTE4JsIo1T?3u_%O@d?(`@1=kIsgYPjW?WivAKBMW+i~gJ z`Q1m{VK)~(_u!MXcZmMhDz?I>Vh&N(%Eh@(1vUk9gGT;AVyKEaWCz(V6d9`iS}c1k z9e03cpBsre%&+K=nzy!8SA>W;{Ml0BBZ4kTnMGU#I9L`^GRdZi}7JcV%ImY7JOXnTCp1Ww~93}_sRF@cVQO%OZ<9`hqZI~2h<3} z)-Z2Q3~4L4QygM-V3Vb5o1hKo#PWd`Vb^Bwc_B}uUT>MZaWUn{=rrp{8jcV5#5`_wpZU7>16F(a>s1rg&pu* zGyK+-!E?$7RdXq)a4WX#4&qY#?RemH_APYcfmag?+zL*M!**;OSdb^L>n|>Gasn}sFaYNyfT<_90{ATflax@+;@q`msm0j<{UUc#U(H{Q@JlNrkIUQT) z(CRUKRh<(E}2rF5p!>&55?U#EADQ~l<{$Q?E#?r-)?y639h%WUAH20vs^hb z$y>#F(>#`r^=i(=TW7$F*6V&6@!n$QFKNf#yMaY=a|L{ni=4?pJ}LG-$R2+0OeJ4O z?7fB9`zB)V$qHY16S4PXqc6FM*!%s0*Uo|EO+TE-c4b?!_rA&OkHws_ex^)ol=`qQ zwReQ{LyEmUx=nvF#u`$^;=7P(Rfqnh{k}ty_N3`OaF8ozdQR&t{FZ_HaFv=mRF{LFuG6^s2Mgr42e3?hBC*6UDl# z?0CH7+_PMVnH%ZD+3Xqk1Y=b!J}+N!c=`Y)@kvhp=HOZSRu1A>d^h#0y#}NcQ;bhA zK;!I1(?))9@sT)sv29*kqLnq|ZNMH!FSfv2CyB+AlV}w!cm31yvCONpWhRc*UpY=sE*&5J zD7F>x&3wh^v#xDeYR5Il#^?Vz(hiPB;`8R7iQ~xTr!XIO9Qmu7582{|IXxpj-|$NH zzt()L`Sj-_3p@+gnwNU)%FNik8$IL4hE)81wa!bOD8^iEOaUJw@%$P8IPpAe{i{#L z7oQ-Wf7yAsmU+mGb<3`o-R0z}K2_c*`(H5^#EYD{Aa`fa1u_4FL2E8P%UlriS6q3* zxtN!pi%cBc_U_ntzkI#o59`~#hUY*D*T?yKivRyMcDxg#n@!9ohf6tS#Ukb7$<|2c zm(w==8l|!YIii&%jFH4KEW6jcMq}G zX%=?hR&oj0g89lPC}u`{!^*Q!`*TS?VHUKW&2`$&Cs?%<>G+ymYlm-v4Hw79NY}O8 z8a{>X_$D^oR^l25vExMZNoap7Hk|05_}5a!;-bWDiD&q*o2@nL$|qD3w=vEn0pQ9zO#O zIJCS(_OLb9H!a>ijcuPZy)U7?kN0A$o?@(uL-(#?`map3X$Y{iKIW5WjG zC~K!uMzVjV;tWyv-V82oyTuUpU+|E zBc3#Fm^fBUY|`pG%AASz)PEcNs4;6U)W6{HlVefbAc|ikKX_~v8aA(kh2Jk~rz z(@SfhcMBf!6u{#Ho}dq&^gQUbauwvT@|CXurZMzC5lrL+ESS8^vk&}8o`s-)KX|FP zX+BO4V<9-S?M;j3$LBCK9^qE`G~sqLxI|y2R0CCS)9(1aEEJoILcbOdL%+x>UoUcS z6>U_KC%YW~u5=Nj;VdrZ^S6#4J9K-cyau#u^)nI+%INR6jRjpoyPunwb|dx}l^1fx z>GY`@P04f7rbUCqsY1k|nD16__OWybUs&TBAkM(NEm!+C9WH0?mRo#5P6ECNT6{r% z0lp9&#y^%04QS7%!>gb{3m)&ff+^78a%gb5MT5X_l{C1#4Sup|aQSB7md*4rGzdM9 zqe1c_m!rY=m;I#k1HWp2sh@awwEkY_PydoVM7p-y_WaO-e#N)Dv5%<3A6!Z7le)9P zSJQR)gB8R;wV(Dkv%Ym#^7C3V_Gi?4dgCYDp5()v)x0maN%>+w_5Y}{eC_+%qu_Oef9gn zHRIkPf3uIhldX4#jN1Lct3G+>!hhi%odxrOI}NLH(X6wHALl%ejqHt*39D-B1t;(3 z*YO^;aA9D3tm_L#K4*PDz`ifQm=#wHSTG;Bw#kAy#Qsy!^WR%KFueOQ>=b|aaeOae z(tC3)n7v8WW_I%X0KPAJC>BCrw%nXvW1w2W0^n|PH75jfoM(5?ww@QP#l&~@p2j}W zyDR`>jGp8l+VAO|9lU#-e3stR_?eHG$Lceko7etv_v?9{+s`xL&icRkX6JtkmRBly zX8*NKN9L79p6~j6LB84=aQnhvGQV}#>N56?1fQx+e;xmRKXZV-^_Mf2DspSWfnkfEo_{E>IHnrGG zO%mr^`FQR)H44SlIyFyE*NbBpTCwSBaKFlDyrTL?&jio6+Rs<>{Bi0SR^7}x7S4MA zAmgleM_-;`pF_>p3GfsKPrcyj7;~grs1uAclkQXlB6>Bsj`EBn-}QXvZ_$KB53GOk z@Q&t9wu>`&-kZ{M=e6OtYP>SW>!r?X1vXic1%ET=Z;$rnz%7_7fH}n4Q1<<|=zG85 z&ET(2ehc2r_q8@JleSf}P)=VCZF`K%_7|S8zYH1hbh^KM`WsnKT1T``!UHpFEVd*y@Squd}P~Ld8fUpB<~85O{ycdWL`QqJlF{TZ`=6&dxW2)d&T*I zyz?)`sAHDwtG8v}y_{jMd0l18J{R&ucoHA{H|C^jy77Y5F7nT$U7ei>KZSR=|M0wL z%K4Xkw5Ob=Xk;UKq5a?he)pFn2dC2(?c_tF#1ReFmApD%ytr-SU);09{owf>>?@l2 z{tS2@zD>Vxnx(JNpWqOUp9Pj}8~;*$CwI8NdVa^`^Dp`7TQayF89ab&oI!v48B4YL zM~=U1zpphp-{$?JQ&=+x>;s%JFFf?-u}2m1QMvq;yn8acX=xQ_gz0y@u`M|3-1k_^ zH)+{PWF$U_zx;;0+Umzy^F{tF)no4ZM)ZqDKs5#jYqf%)1T@Kgr6GZ;UDo{3-8JI$$U@m=v}>M z$>czKt%~HLY8-UF-ci<8s-|2u*e5xUey4f9&Z518j7c^8BhQ0(?Qa(4((|IZ82J{}X-bc254QT6S$DUg$F`2> zv8vWtk>pa**tGo0QHA5rZW+ zn4e8sf~WdVJ_NRh{q0SVQo7dVihFdM3#= z`2_>lMlK}5wRjWRjh-;5W%R_UVH_i)glhvlOZN!ZdQP|&UmbTxE{vyL;rdFnJHl(w z?lEYn5LyzhKMDODWxWK3I=yDZ~ku_Cx7Y3X8q&uyJ&kApMLXO-xm)`mekjT@5b+b3R}ps zC9&nhVR%rsyt_)^vwC9i4ZsyLZ)C68hWsqnmV|h> z8otAJwfIi-N^B*t3;#8jeAR5;(=+1DgJyP2vi}6n63+>HKWCI#d}^lo)O!Ow>NOre zo*Nl*>P<5D-_V{3;?HJyGt8L9Z!0e3*2>1J911Lv&*TCx4o{BSg2sql=y4rAQo@-E zEB5Er79T?A@oqJ+$`%Bsm_f~!^Ir8lGoTn*2{Q2n@~4Xbw9c>|`6gZy&pFRJHlW9b z(F2TLU@Qkd*?i%lyd^8(tqpt^e9n75;PC@bIq>*^hk1Hnm3)6-iC||t&#jtg1S055 z=l8h&Sl=+VzffJ?64^d>eqpP%N9 z*r^G4<3Te&Hq~?cYTna16&7!->?HQ+4_C4$LYg;jp>1#Ya=hU)@4j(Wj-{i{Wkc`M zHS)F4jX~_l2eQcl=46Xcys!`a!;HS``=tvlx^ShQ{$k3-vKm#6KU zZ?tLqt}rmCY1_1Eo3rgc32oy9V@=%sxAJqaffJoKXH)^+73GOQ{DO7&~_R| zXzea9FqU61ZQ6ECOxwWY2cB}^@d1x$dmr%Fv^@gLs?N*Nw$-0#du)Go^fv)*`#x!Z zA*;WU{;x#y^Daa4mL4Fk6S4Kdc1s@=-uUlqnpb?_gf~s|T6<+wynCa^NYg!h0j*oV zE!vk35baYlJ3{-BOxkA;+FMOT+3$!P_kos_x_AQ-I9mtP)r{2DrcXURF zMfVMDm(acX_ZfHJDB7PW#^9Gea7D`QgO1m5&4Tu~k`MO~tKDaM!ml$XYTN^Bh!3yz zMauNQeqs-5kwo=O^-hr0_S_cxpCapunFV#HjDe{KZ z#^v}#`*(;>cHn1wr(T~gJ_$3{6=lS(psy;kY{MD?&T`t$}7sKDXp18w*U+-Ere2r0i zh_Ma(*c$Ug6&OtJwL}u+RH0HRe00Gi!dWc1sH}oNd$h zC5lSx_lQ0?TkN_{Y-Yu}*_)(vA2E)mFD!2NT!0aXLj&R6VE8t;G=G{cz)A?DDAcCIc#F9FXWzLlX}iGw7B2%xUrP} zULl64_-^>9dn$WoT$faBP7`o9gUckao4{Rc`Z)W0YSK$xX!+F7q`cHYPoLTx^q8*r zXX|$OHo-qFJBGja0etdHVsK5wHx<{wcgk0MS+pvA32*0Y3H~&Y3H~&X$v?=u~*Vb zx99B@8hcM$BGl7r#S0Wa)wzJbO*=l?DW;uOVWU&JLowL#uyl=!!KPv1yl%JGf+a*7 z9@_8?-`iheM%ovBef_!BuI$!VnfKGw{hwh^3--Go9R8y5au0FnwMG9Je8l&e*3JAT zSD!kn=YF-m9~-EbbCC{n4Yu0Pz5{P(`8g{T85*ayN_89|)7nd?Rk}ts#*4A7@b`m? zjbw41cHbRNk>`8|pI`d|G~?H(55GU{^Diojl(AM6UE}wy!e_3+&#xrjt^KEd{qO>0 zlFrBeTkCvmM#e z_lug1d?apLyApY*y{ffGLZNb2s?Uf2wiCBioOTCsTg5pQuT@^jk)795x&Q%b3R3!NinWPtO-#p>z$vNwcWHG<*e6kF0D!KM!v?0 zy=pzlEx?t`h^0vGYkiWV7nU$K=>=>ZOE2sIFW3`FJ#&KCr}A`}{}+?f^GX*$hxnk$ zXVrd2d}IePUaNgu50ue9v9|DJo{JM3c3^8Wv;CWp`^nv9oqMNxQ?sed=$^WXJTGfp z$vbY}_)qt6*4UO~1r760mq#w7&r-}yRbFpAVC}WUej|^9W7Vk*at%|Dt=j7|*e179 zdtKl%YE@(Y7Ux;_F(;ha8nf!wpp`Id!6QY~dS7dH4-7}jQhV8B>IUPEfxSkV_N@M| zz&a9VA{MDQ(@tX2HN>J7=UoK;72^$2=dJlse4>@_igSdBM;5?$#cwoQIz;OZ!o=Sc zlRibPGfQzmJ9hjUwnU2diA`Gj?XRKT6l<8Z-dt@*?e?kLvi2Z}62H_QW$E_5dP#dv z{}%22-h^ZQ%GHk5(ocUwTe+_6lC{uj1H7s^I0cWyd$L;x=3LWxY}%{y7w`-+I4}!3 z9~mz-#iI3DHoaGYN9WvD;>DIe5Kg5JDy384N$mGmp8g%$DyFR}>AC4f=aaOv78yq=-vk#!#LBTe%>9-csK9n!xmk{*>lK=Ij7h= zCjI{8&SRf>b^bA8>B_gZ)a~@8h^4QEmfwN)Wz#4&Egwiad?&nlkQ~caY#QkR=p(rr z{!6hJ)JzM8qeo5>M?VW~Jpyg5fwr{H;5F>o)6kaAG2ac%z6EWaX5S?34b-N!SzJ1& zgBU^bBysd^YP%FGJI&lY!FWcVSMHWI3{TkSdh7%Tr=~oSILR6yba3j7JL~Ow=BBmh ziNsFM1lKufYu~7Ce->T+h%3+1)r0Ola-UO@rTLuw_xovGeVDl3O4jrZqL1NSOIL?j zClfOBdZ{lt%ryusmR?tla23AXX5dmCv|{gVtg~g`o>s*{Bp(%{f367M@Quip-Tb!* z|1Zx~>f4KtcmkMpUaoxPy`N+KCwTO+1||MRZGZHJf_%kbeDjSm595y_dlgGm{GOcl z7I<5=em?f=N`}F&ml%GC7(RODf{z$JF+yVaoca4F#PIDif5*qU4=~S)EjxOpiTL1%v-YK6!UtN`DVRLEN;)2WTMuYoPrLQ*mJA> zxHj|sQCqG=nZq`&6#PamK6z%!Q;FI6n=Cqz>{7111^HEO8u89jawXK4rENps+v2|p zc%~4Zxl$Z`{8(4nW4%G+%+N!^fyOC&CZGSt%Ug~0YUFJp^7glYD{R4)p}$7pLVtZq zaA60H?)CA$?VAq$sjKt&G~)g20pj?kiueEIzYKrxI{eb_;+O8luT;E$FMg@w{d@6C zEgHslb>jVx+wmRc1Lx%!bM;O--v2Etrn#E?9^&nm%;dY`{oEUg>nI=SA>MDrhV3|i zv&)$0J;Zs*=v5SVfITzwedxIQL#%5>wntf`C3~abkaAz_w^rYfA0_UO4G}xxmjBk0 zxQ2GI$--I};~_5)x{$lX!!u!Y{)(wnQ%PdVk*O`g5VUq@U8m*isUG-0YCQ;e5X}2O zy0?E8awP7`YV{3$vt4$-_P{%hERik$jx8S+75(4AwZ5sXYxrGn=2)_%8@nI-r&s&t zY0X`Y{p?n7cgRN_|4-p0AL;rp%Fo3|oM78e}~9x~_nkTJ<;z&#Px zKp!yAA&0OD+0uq=(0Q*;4q*TpAvqEzhu~w(Td`pkS2}QgTAs`zH{r;W7M}GWPn4r3 z*7J%7c>*Dp6)V>;ja958Q1!Q6$a(39a&kXarJN&mN9!1wAbB9U;Pm16 z7@4q&r!xJT@P%~lL1S=afb+hOe1IP~R|BIT7}akZc2|^q!)oA?t<{WNk~~WD{P_L5 zM{#8LJL21f9#7{WM*7Y0X`DPn+{TUC7dZ-js4pL@tjSn=MfH0L5B_?JVhZTd z0`%xJnR>MF#?=2gF&|p`6FnM{Er>2~@+2KLAASvfnwOR4!_hoRnis!f@t^V}X?|2) zj6t5HErTDII{c{EI{A^6^do*Om}xwwzP&SjqkO47d04+uKa==6G^zC-QO>0lZ&JGx zPMQt*9&#t>rq~#MB&QN8GnVK-haYS3d&Q4;nMUka>tFIICQY370j)dv-an*yQaP3+ zdeKXsrMS*N#gE>MBV$_eV^-Cayv*LuPf@Ry(w-O6lhTJJvkhzQp>%9DR~EcAe7ymE z%qEYL74DL5yvE2sbPe`7FtAp<3||ht$l4TW$4j2k(T&PCN;h6l{{BEtS}$fz9;+8a zMLza%Mh_M!=f0rx*Ha=FJ}z!H5npD;x|O$39LtGg7ZMAu$cUpk^^Sh_ys~P}iB(3? zhqJkqXU}4fqZR1IYIK3G@KBbq_dMQjlC+=WxEca*}vk3iY zSa$PRU8Q~TE6Evo?L5px<0v67Q#s$5QvXFGt=H=oUm|1tc?KM$R~Jdw{!u{85uF7af`B z2j(gpW{s&Bm^0-~4EVBuc{VUF1l|JrPhyAo$sY+$#XnkruM!`m9NwQOhIttnM&!<= zFyxMrJ(q=nT#^k#(1sys!!RO$GS4SaOd7$%N<8T-r6pzc-qi!=*0nLVqM|aLJ4*LvW zSL&{)3!apGqJ?{l2}l7Hg$=AdbM!O7_P z2jhLyRQ>rQvl@PV`2w%{&wB#6kLqMXB=0Z zVCw>4)>#X}0dQaZ8vjRm*30jz{gLNYN5Qj4!aS?^y6kN6%v|o5H2ivtP{1* zAP%40gkPK@Zgz%v9eOv}ja^$$tgw}5wAXbfu{Ddgfl2$>g&A)(@M|Av%@<=ow}NqN z9z)bAX^#BhLvU-4AW5-gR>$(@*M$J`6Ruc;X@9YBvU*EDdm}BgC3_d4zYYbMKIez^2;9k>sj5uUa zvAxW>QEaK|l`r=rV{fMaEoaZru|Ff6iKm4zO|5L(0c<*BTS&V%JeAEd(pu3AU z{sY2)96TsqQN#I!VekO{{qmvMlP8}!u-BShVGc~@;G>D>;A_?#e4{`5-Siw}O?vgn ztws~;B830199bQR1Tyh|b-4cq>kU7X#{EYbxaa(j5!`=t1>Da!0{^RMe+2i8TVqb+ z{u>kE{wovU{!wrrm=O0P{$RtOeK6ikOzJbYaa@V!g1x3e8@4$o2sE$zrR@f z4(1Jp4x7mk^N}fUM27>a5kaPwqjMXfY43-z{?P12dy4q4w_fXb{Bhz6mQ49K-18%U zsCjBFA{P{$&9lfNts5-)mexf0#Y-+p4cm8QpU(XEApkc9)ua>>uo(5im_ zUoa1OuOd^kQsD>9u<#H<4tU{ry{FjJ2^$Z{A{!5@EZ$aKOK+~$m-yq7*E-KCEw^?o z1uu$sT74sjgPywFWyN(4&!pikIi6m(VB;mM^F0T^O9{TWWUKH}@|Wm3;2Z!qHOM1; zt0VrrSI^O>BP#bVPs{RSKs6Q2wi++(}nqQbn(r8|6AaM*nn^n3VFg_ zi#D1Vlh0qmm@lP^Z(96zlkj1t>B1{~z;CSEyeDcl;>z8$i0$ZjiHI~RPK3+ z2}l=cUq<>rH-J&c)NeffSSC%R>F{anRU=>bTcBm>s!Qef1bDd-yu5oUUY=%7 z!$URwC7m_>p&x*kNLpquWPXJU_5<{jtM_XSeHArom*RoJ~o zKqd?$7>kasO%bL4i?mZefo%88zDyJIn6hQ*P$OtFir}#4v@^O62m1 zEAFqMGuDQWkQ?9P#ik1leY3xOD0lE(@MFaX4}Y`YgKdh9;n#kIp9KcpFCQuxe1>N{ z{IC58WbX?%$63>^HT7CM9r~#mP`!|Fh)*5xAxBlaT|#@{CKfe~rFyp#Jh^yRaVFK5 zUuw&z=^+zGR;+abxS!U%HvHml`8NB_29v1gQ5<<9WM< z=b(jW+PgfSIl~S=bF9xa5uPuFS2*^b{l$^uud`1zW1l7*XTnK^X*aCWqK40>2zf*vS>(cQvYrgs4nQy`Bq4wB2KW|x- z_<>^Fp*6rojN9U^pJvQCZC+{4wYER)A75s!$H8jNHTxYD|JU?fOID77^Xtn;=2~_` zr46Sw*BNkz|LX+v4XmGRz61Hn9bH+DU5aOGzK;TXr>9kP=)i5w_m}>iHQzbhZ(?6- z;WiVmVdB8r>miID6pZ$M4@no#iw@KCF1SAVyj$=XKU`Wo&tuuLW8kTpvdqKT)1tps z6Tw3+Nb!AN-Vc{nJt%m*OS!@p9N4p-I`5eMMjZMpTvymp@M>Y|I6A4)FkW%uRnGq< zAJ(^5b7if2t>ZZLrdxSt_BvNbnBVa_R|omTuH1F*j+}L`cVI&&yz9IjxAFTJ_ZO1O z_SfCpvETG2X7e39b%klm;C*VE5?+_Li#i{2>%P=VGkWm^FfccR5L+m@|;i4vSAb$C0!-iqTcs9zjQJ$^2!n4Uj!)R5TUc>y@c6rvXHhK1n z?Rt!7(fOWu127(S8?lMT#2#TsXe+q`{PIldnA?1NqUW5ml<~E-4l3O@>ZG<}Z(RhY4UwY4k zM>5Z@wa#ER+!tR1C(=9O&zo*AUTBzPbpAchy8Y0Z&PE7m{hc?qinAAVhDwYWcQ#j; zYZY-U_G?ILPu}XOk#k3BE9KVNHCk`BKcRSAw|rz`Fka#=ySS(y4xe&ohhyITiAOrU z9rBIYYpRua-QlzRe-``gQ9d7K@7zc0vN{eo<|YeVfzo8|95RHxZC-^#PIimtKODEqf>Z(gfW+M2KEB#@G6C4&d#v>WQ}T&q1ACvYPQx7Z&#Z zCs)U8^hTGNliWn#IXo99&RXqdpIpZMmNn*_aA|k8HP%vUyEWEA#tO~7Sjhh?8Eau( zZil%ckSs7vYpnha{-p5Z_Z>>q_Kd!(u{!UrwBKFHyFHDg?;c{@t6$9OAhw^-IDen9 zwXD-VUS8_4w1>umPWUN(p4+m&f$!4i6WxI<{|{V010NjibRfiKm{U@fl;jtgQ{JxT z(`8V0~XAPgzkwf<)hlW2I{@(FhBAp%+*ti6n;L@HZ?^7S>jLDH5%xkz% zy45hI_|}=r3MN^5==qAiRj}+RbXbg!Q%oK(bN>zJ-4HrW`+P0rO1WL|y%DZlx2>c4 zCZj`q8ZS$BY^v+-Xr7tuNLG}zh8kn7m31qhFKUdoX6`fMwC7uwx6C6ykE^84Gi?^X zvuQh2=bLsbpH19r;d`he=HFRoPCHm?O!E|l{8bfIe*gTuJBlkR{nd?G$-+iYt0PO_ zU-sWd)&O0YraUq0fZj8nDg&S2;`%lERO^6#g`R)kJt^rmZa7eBd=?zv0FG}+Rr6hI zg(}P&-gcRvO=+HHn4f+72jJP`zG2MVde{B5L%P7fZfb}3rt3P2Z<^Y1!sSoxb9vsb zroMFOH}`g4m=?J(x(48lF*KmLXk;%eYToCDITwYv-}0E571R1U^to!x!?M}X&tc`Q z*HDwSian`RgX}5X7xdqBh?#^oL_XEPZ_l(wc|XGOVU6TP3QyWX8)JoSUL?z%9M z?$Ve{=np=>WXzrRm580i?km1&XK5$rZ_@AfnSOuXHO7*6Dk8P-0=L?*-d&$Pt%2vAcl`@I z{?m-*HEg`5irld0Y2P%@%>DQq{g;|!nJh~b66f0pY$#(0LS%X=_b`(l0Sph_TK9 z&i5GW!#wveV|})l+wRg5R7qp>vd$gl0Ael~2_^gQu_^U5ecq{&?$5Vw9#oCfl#L(Ua_Yz2NRj zi&lK$d%&mH+Y>C=ZcZz<=wn|HJ2-}&SDSUyLDnhn3yMC38wXdriN9h42C>tE*nYuh z8CN&|*ZyQI&I-X9c%QeD|FMDi%$a6_Gi)Gh+_?*K!;kz#^l>m4_?yvpMIYr0()3}_ zhTb*S=S`}S46mA=q-uA?8&U2R zV)IQT$II_AYP-Q>aTa+HWV)q~cn|w}t_MBN-f1zNMXvM5{DnQl;(CJAR0ffU;cCWR zSTVW}m?JC1d*$nl+D)|Unb{-xgUw}J$GpuQpgk}5EdT$eo?wV^s8&j_|v&RN>R27*{Nt+wC`nW5GtVv*|YCtFJ?>; zk}cUn5hWo?3(7M8>&%?-j2@on^Zot4|Nrm%`oDhn^BnK7bIx^^``pXixqg%R zifwRdGkN!4zTRDuNU$zSqt#GujMoR<5OF>HnJ)JW$$txdLhem;!}AoFT-y9c8^(XO zLHdiuzJ%Xa`+FTsP_Bx;;bfcO_XL}aCk;BDaNnroi?-EpJ+{aG zV`KjT?gyLNmCl(*_)gu<9n%2MZ%xx(0p7&`tFZ#-xcvHAwcGD?rPm-E(bk{SeJlPU z{byPG70UWUy`*LRL%l&Y>*qU3avvINW<6nH_}xAmP~NodfZtthDl^7!=&~^U)&|O( z>H`>cfhp`(kkJ=!AsTl1d%D+lb|?LDs51_A#{Eg>pKPe{c%yRrtr~CA&0&;D8w~i~ zBpG}7j{VPX2FQJ2g8PJCI4_C5so*-f7ne7e#TqrrS{66`ZhsGD|7u4~z{``CY3-2r zZ+U7Jt`)rj(pA-LGF5RuUyF0K4)?BP{bb=g)5Z8V5Cr&~1^4rr#`rr#Ogp5b3=O!) zOb=bsB3X5l9qvPP4grgKh<5TxT&<^dgKke&1JRFHhX&QXcKA0gn1?ujrO%20-_umQ zwp83l%IjjUOztg0KK|Ck)D6o5fvhZAPC8&2q+V*k%VbzOzGP2Gp0nYe3ZLoVcf{0q z4Fc(X5MHJ$rju{>5xF()$<o&9a8QE=CDd2Ly6(evVtMIUR+6+Vg*q$lpXI-Y zk-y7J-s5HPTC@_+(b>)M@AYBa9e;C*{z>vyx5>=KbK);{cglQ0?wfk6D`k@J2r{bp z-YwpPVKni#F!*~`Z_G>j!&qJFgTLy8zLvLNm-tu(P7lO755jQfm|ere?RE{#&|Zo< zuVDawdqJSY%S=;Z9+KbdWFhkK+}d9vIUT3M``KspcpV>x<;7uL<77VnZFzcFUOoDo zlromhBWLqJ?y`sXoGx<;SAnyi7}Lpwe%h6Pu2By4RSlgSJHjJZ{EvQ1zz+) zhn4u8NX^$B?W=MGnd*n)#^b%$m1_96#>sQ$o#edHmlxgDOf|Tq-Hs0AJWa+hL86cM zOI8}U#J{19&rM8Cc+ogVSF+t9ne0Pw&m0VCkc{^%CaP(oJy3gxd&!CFs=Q3p(;)mM z?whI2Lj-^KjpUmPB^$f4;bR?+?Oc5KLlymS2)#Ep zz16V(4Awsz>!F8rU61EHHLNS{XIAQBT}ztj;(a8YLrVB;sz3S^{X5_czKfl*3zEBG zeMz}Qr|Gw5ah^%*VS@EA#(H$cdf+{QiN;tD4fG@a9T5EcYP`&@_?$l*>tRp*NPY)q zkM%G{pW(Yo(Q2S}1MkbOcnp>b*bMp&~NzOjt1dP{q{Sb;JN*`KFP*DmHULO z8Gv>9rCwYq0q+q8tM0^mbXcDHHr|i~NuNOi z9LJyxcN=j2;q|ZdTf5S6^_%i{{-G@|6vpRb(iY4MVB4TLrheB?FaFR^-B2#?CuyHy zd*Wz6;ky_s)v>R9KK?bnO|VV4#!l?Dcvgn=of#Z2y{%_utiFPnD}9q?b;IkU zV4S?kfbH><+R*eRbI%|DW*$Bh!!e}kOX`O2N3AsJ3(2O7T{F~KFpymDVxKshILl;s zju?pV%Vy@`oFMlDdZP^!;@{@}$A za|#Iz;)jGqgYg)J=z5e ziF}yJGzb{a4-0V*2=xpM@rMBG2zMVp_t}1YFeYiXKA}N@p@d9A!uRwE;7c?9-PD*s zcMpEZu)kMmx{YEjWyM0kaQ8Xhe8&(UFP{K+zhA6PkTngnVfiEZVNy+xe^ND>ZUX^! zQP_$(VN&}x(P8}1UyRb$5=E!*5HG&e=r0{RkgFJ{i-$6faHrr;&~WOg{h2=8z!{(;eu1oH>2eq_Gnn zX0jFLo75gZl>KVYiHg&EhqC!0A%P*>o*wG(*HpI&4D{o>2S9|oUpU{<6O5-R4kwyA zOfdtFelY%J){zOJs50E!olF*f2pA7nwDwc3s64?tFa+nq1Vu%uOi{@pAdK%t>^Uf^ zQK_Qhn7{xpFdn6-MMR1!Bm4s0vA_|EI#QXUl5JqPbd|DI)RGB@!EBk&CJ~pxY;3$N zW^7^ubFk@hHXK4p+vIct_T5k70Df34dD1BGB$blW5#S*$Ue58UHXcr%O1PcGi~csv zr0DiS#A@y~zZ-TBWA_YV2Zo2S13lSzhVkO}W{0`s;v0siE==mpjwTB&Az|Kp zb_D4x6l3Am9v&h5&`^@y+dY&W5XkoNclY9(h2jc~HN@sj@(BqGclUD+=Z8eIJ@J&q z_h91*3~R(D-5tut!rgyb`6~mSOvBKKJWHfcm^XV)pa-9A5!Eva#$sI}%_0X4V9Q-d zOoxZ?hvGIE-@cOi9RTmS{Bq#+@;@GUeJ4Mp{Uz^zIm#1%#Yo;i^IuZ|n(D7$nri&Y zB{9G9cD%W7cOM`3WrAtk)*_W!yC1*i7XmZg{c#kIPBy%D_p^O}cE>|o%9wA9p+2?s_{|BybxD2uyk<(DPu2=5 z)@Y^Q%Ve7Atv8I9Qr6de%jY_6I@dg@nN`U+eV-@UUe+45kdMV+PNE8N2$u>(}DRFY@m_{KVW^xvk{F!+twj zXD_jL^lV_xbZMh-e3REVX>+8|Ps_PPOq(n)w2K=SVg6Fi-7ajORC{~-3pr03Z}4@d zWs6dIe!;p(ld1bFUNQO)KJUx#+aW7eY2mnx8fXnWW9YD+36i~HfdY) z?614TpR4+teNKPR+^WfpKJ@b1^f%q!u3qR+&0Ny`)b`wM^#K0e0g(&qYZ&wEmtJqb z?b>VBx$z#m7uGW$y7!2vKV6^O!*ABWfbL(Ihhuy?p5H0|zmb{XqIi6l*N--$> zKXRW=X4b_9yeTzY!u+s4g^f7O%#l|A!Owcr@~q@`X#Nr|trwLyFn`q>oJhT;)z)&x&*y;~jt4Ss2r{tfufg)KF+tPy zldg|{%-D22{y9FA()o{!7FQkrIZk$Y7izxL%p~<9M^KjP&J3GZ+4A(PR$9ZB8+qr6 zFGbuq&~xzqV*6pCRx!s4nEF-1(K%~&U%K8Ran#0jPsE$29yI@S#(94HD#_~iU0yP# zN473csWQJ-A0PR(%;=4ndm=}&gViBT_J0+VIz!ynFI)b9z1UuH`nfd9%a_#({;<9S zRIfbxNYb8rSgxP{&B(A-`-s!>_LQ7YVSmU77Rl2ser)hM?a=3{I4CNozQ^f=Z5z~^ zMfP5Qn_0E$;WN{X8;cf8+xJkc#^*L_W?k8szkH~>f%`LN-LWtqo6sMY<0i&T@;LTN z{KG5QrPGl{%TPa;q0fRVnPs29w5N^3Cf0~!$Ta?We36&`vpv5w*X5gd;g1uOOFLh) zdf=Xoc1*?lz7PY8nFSB-OdI<675ClNUJ2!|rd@Q9X1u~A>D@rRMyp6?;>1bfF;e+n z=Duaz#W#%yL^&r;i?)C0W_5J@R)OQ9wHM`fHGUVjrguB{u6Ctc<#}u2_*ZNxXQL=68wYx!7iC^A{6b_Wm!= zd+L}!z~A>9^7hmAa^-%I8<6`$-hO$zbdsEOU$!oH#T^TxW??b8kMiM`SQaxBr#Jt` zQwH32IO*8lV+Qs5*rxW!RPKYLoAWGFF8pixVA=S?#=M-d@Vd5ca;-Tx$?ly*BDo`; zZsCkAdcf?+Re2ST1$M^B$TS#|8_TW%A$JD@!CI@olDRh^)U*|Cf9+`Ep)?a9(k*9?P?wEzBNh zIjqR~v80!5JVFWSXK@54dSKO%x7_?a7ejmods=<;Ex2MWx^S`gv!2K2PHMxf6J5l0 z)l+OG?dK~$$9>b9%eaL3udXlu)%8oRFJIUGw9fzAdh>7V)vx=rzxHpkbxOWJ`)l9! z2Y!sB*t>T>^_uCQw6l}VcpN095P|tAFQb3@2#cxME;QeKT3?h8+FNtzwDvB%>-+;uPIQ6_tNlfJTq&i@FXhqQpT;}hVk?_214!WC3!t^$i@lVJY05m0^D z7udo(FsN=blpgF1v)q$GwPQIf+r)=7T~}De^9Dho1dKw*f{IFC$eW}G*AIlk{FiRv zo0tXdb4J1~ugjoTQ3O+$n!?J69N5=s1K1ufhW3;1!d|Q9(7c5N`Y&SOQojmFb$kXF z7WM-}?q%rWTLnA5%)$>34uzPSJovbxGb~8z26GJ-z=MEBurP}Ry(vNA-@-{`Pa&(N_YV zUFO23$?kCE>quA>QwZJp=b&FjJd}&wpepAU_zhBnnaq7?`|J@+8TJgGZCMKi-HpL` zc?GmpnVjdprn?4-~*9_f?>MJra}+bwPW?7RZ^J1G!ghVeH9dfOfTT zfA~=_ad-vaA7}ww{03c8ZbH!Q51?aJ50xJUaO(ACxc0>u)-_Lo>|iS}S33n7cC`?v zy%i3#=Y#o~9WZk3C79o@4`{mhL&*Ll@O|AC`rg<8(QQY8b@WT9dmaxJDI4Ksc`R(| zyAk5v`+}?MAh__o71(b+4PJZvAUMPZR?X}OJM=O@_(}~{_3a6t!-jxG^G?uG6F(TK z>J7a=Oo5Syk|6B05{Moez~_bgAizEZcC@X6)hYYnbn+*-JHZ+5sMi2HwLjF4UIZuS zctf$3EAVDEgJtnC;4276?If1wuiv+oDf$ArVU;QL@VF#t5wvca?-eyLH!I0A}K$EQpg_oy8{n{M3dUQ05%i9b42j7D7gM;Ak+7u|Z z@rL$uo9y=lns}UM#6IYC-8Y@8eA3B!IA1dP+Ykgm>ELIf94NK*Sdn~)3dO<=TLy- zo8jn$b#QjFGMJd1hS7pW@TA3lh&a>==1<)TZM99I=$siezWNUFPk%uC#qUrS;Rh-Q zzk!t?2$lut!S@vsFt%y|?q2=DRDU^)6}`pzbR5?B2f%u-A<$Pm9bzBZgT?c&@Tn0mz z2j`&w#~jcJaD|x`Ww6ZrAap)?8Fuy?1ghuuz}pkJMt$xEYkbPUND>K}%>=lvn}Or4 zG+^)D1gCc0gFZ9y53x4bK$|BwVBy7l*zz-Rfa1zYVZj_(Ed(5jdHZ56&+3 z(B*3@SRZm3cB>47EZ#s+DVh%@b~j<1Nh3VW%Y}+sYgpj%47PX_!sCm5Vb8|P5IN;O z_>^9O>L)sIekcb<=#K!Ga91c0)kDDAt8m18CbY7B51p)Rz&&RLn0$W&uOHSz_XT+n zm^=_vORj+A(JBa-`x4T>Uxjt5*|1<{82BDN3AX~afmO(SxY}kee7k=shVOHr+0ThwM!7oplJT zOt(TC*PHNNV-&oo`wFXv`NPZQz2VLMSon7OAXI(03+4l_L!Tk;VA;(UIzG;Y#oyH7 z>!`WVr&A$JI2{Y)HYLKu?l-~Ir3kXFJ_L>S8=yl_2KcP^07LxsKQsOV#I$J*b!XbZ zw;S~^Kxl^H160hs3B|MAz-#puSlhn}uD8no_bFT8#bhBkFTDzdR$5RTY!7Cgis88a zPUzNq1Wc`c1dr#X!uE+-;JCaICaHXbSJB5HzWX4sJlY++))j$E#UWT?XbE|*w}5lY zJ+P_&2{`zu75Fr_2mZ-wh>6<|abq?@!`(s9ZS5(Tzho4o-suLs9Ys(y(gj|G4F#`v zHt=KHbC`AC73O>Gf%8Y^fy<{|&?~M7Sf6)+sR63c&2%{o8MzeVOUvQa_WR)Ql?ReZ zZ=h4_Jh(D70a6-!!M*G&(E7?9cslJEjJ&cOmJOc`-Ifo7-q9(b<-P(;$IJ&o?+!2= zKhzv&m;_OrcsP(#2CtsZhn%68z_9KueD=aIuvQHG-MxUj?gWSyXuuxlA}}hR13oW` zVRK4*IAF3Kyl)$W+WXUByMqnmQu82a$sAa;pa-<9bnSA*`P*U;^wDxASRP2i%j zu(!Q4oM|`=OP)W34aM%zc90U5xdWE&+6f`={b9(ed9WnoAo%v10V9m2;~S0dp#UP` zTb&EArjCV@U;*xAfrt0^rPrxuwqaJt1*s#xkgHlZ=bKUFIfP(o$7+2306ccjd6tBC zUs8&%gadT0^%>}Vy87gX6SnCM$Ic#&J~I5U?xA-Fa`zwFw|1{!Pv~ynUEVwArUmVo zzdd2wwykHj+}&KgNoUj0RQ|?w8*#EFSV zR<>B_k&qMLGd@1_*MBx&`%=hu-9 zp`Q=czpWimv!c56!^roi-Wk2!U19Y4{c&m5JJcrO2JyR&X*c04P&Ts;8q4o-c_dz#sMB@dUF1>5meVsHitnDjqTz6Ya6yvG%!lIhntl3@ zOhyj+5&23F#XW4g>+c9go|Lpa@4y4`<1TwQIgUr})NIml#|PpZgEfaH^+(S0y*&8S zeX(xQHs4daB>lkm{oC$~$IQ4o-s4j}0|(-VthKu@cI=&e#;Xu{M*5Jt_xHrllBVAf zo<|%zMyUNLEe_9Ovv!HBAtZwkcsd|%(*I0t!&g=NQ- z0`a<-{V#itL>~7f@rqS}c&VL=jl>jrl99=aH+RL)MJJzjZ;kwmeJ}s@cg2sM4H&tm zwhnpbW-sfzVxIA{JHrc+r&gupzt0!v?vBc`5+T=ms=s%0zBr+0$>o?`$TQ}QFCUpN z9uRxqpm`kf>g9Ib-`x>wn6>YC+aGy)yYNvX?}#Vr54rbv0&)v2uI--N;%Tv`ZHSdymKwAMU6*uhT)omkJZoFN-bC=(>fh zBizAvQ^(8V`kj*}H!eiZ+mkeL#U-(LSm!OLypg9xf61)P7K`fFEZsJN@ZLRh-Ll2Q zRIeX8XN8>od2-C{Eb&?1!Cov=lK#V?h?6DmXMJ-+v>tN7XrXAbSiI$1L1#7#IghtN zb?`;8hW%KFy2_8p8>~{>iNrqqHRJ0Gk*5`o^J=&tUa)9#n;ut?yY*p57o8WsF}xAB z-~@7k@$1W3nc}k@dTX+_BhLysHDX_eSQK=2etH6OIC6@$>YVtju&QT5B;l5JAH&Xy zpY$CwaFqwizrw%OtTW?Iv{7yP94$eg!uS?*e73GAcumvevQY)r+mk?{!m|y?TH+<>d8^@ z?H#5wRmzaVva(9yVR5&;z312#AkWg~#qT^Q?vVO&!Tv15mHJPK+b_O5=<3ow$B_#j zdd&*nD;}bgmilxX;e3aL8N0>LUynb2I1xEd&!f~~r}(S!xIR1Plk|5>vO>0t+pigz zcxEo)!ZXNx=`P7r7$LnVA*uuQlNp6<= z<^%FJw+qvEGKWvxh!Pw_p2fB`l(!`YJU%ok_hA`HGeI9{Z_m_=1J%PO9}MXqmfYwlHM7uT(C=i|teJS)HO zt}*+k++)g@kn|4qS$et5%22f`-ely0PZLevkc=c&PtNBEpSpEbjI`VA)O#I@!oeH>dNPimK?=5mMG_DHgNe0c?~ z?_IV1TIJ(Um~Za5Ek>Tdu2n{GKGW**#V1*tkOwW_?t4F<`P#0>A)_#oKDz&kL3bI| z&8LolBXav5UM*AaGA>s93I|ibIq2GPR_pBoew8|OIbem*9rA3_er^hw}i*m+#CfL6fui`}APiYBG<;=Og z50~$cS;&Pb>=|7kznp2a&S6_8W+}(&Q*-=`eK|A1>}3aK!6Knx$hiLT+AuI#Qq^U{+i)?+%##oC`bs`nfH=AubECO=c|SY1j2|8hHoTQubE>F z+x(5QR&gqJZ&e1Z3a0;TLc1tT2D_Z5rd zIalLt*br2~gwJnN%NMT{Do5AjXR<4p$v3#W0w7-aTv^y1(kmGC(IuA8VWBXgqvk8g zoeHM;4#|jMh!YN9o%~HwRl!&W&C8#^cq!+aYhxGCdc!n3{X8PsDxQ-$e}oQLykR1g zQb&&y#Bc&zd?=SVzG2K~OCDD*S|hxx&3PsXe8Zf#G~94#NszE*f?b_t%^RlbfJbsi z!3v?3d1|rb_#0;Mx@id|ycI&vuMXILGJdyYtKMcf&RajSq52J@K0VIp5R)KONw;eQ zT5p+2Po`|LmxOT|KV4`67H=7S|6U1)FD7!LJ8#qir?-qnbX0NPk2sFKd3|RHddoaL zT*!^EULzdee`+U4ddp}>T#8i^uNIEBax{hXw~Q+z;V6Ii5#H@`?2+WoTjq37lJ;w> zCBi6W=MR#qx6JNmtxBv^=5nUD z`?=>*JhE4EN+&mdl=P`&-e0v?RrSL|Xiz3Dk~mf};Ug=B-b}3UbLPbNlE6x)vSdQ_ z?nSYj2WvKA|5q{}*EXzDSsKdO+^uIfNUvm+Tj_hO{_ZW@IwGZ5a;K6Bd3&wX#YL+* z5jo%AN~$WEA8|X54!O9R)9cITwxIQ%37(dDAbDXpr}O95Ey3bF^W<8PPf~U~C#QAv zTZ!X)M)Sv%D}!525x!bcWDY^^8Qa#`V;>4)golacL7;b7XM>CuP8rdWmBd zGyK}4`_@bX$9K%ZZ<4?&COj&&Q=QUmp>Y%=zPlG} zZqEu7ZZC`M1o>6Wi6gg1e^?yM>3g~4v!tqunYJQ`HMn_zFu;vpCuv>HJW=oDaCA|E zaLT;pwUR#7Ov%Y_mwjS33fr~(_Fdvw%}h_ZG2n-IBWKCjTg@P-nz=ElENdw&;Z)qH z!SPqkbO;}O@wvob$bW)2d(*3#cZTK1Vwhy1<-I*D$ggHT@34<GP4P8uZGWVXO4?5q}MP?U2FO;5v&ys8~@{-zLurMIq&^rJOmZ z*T0rH)-luKPiFW?;yESZ$~gY(n29x)ww%aXFMORHSS49g$Ap*;tx@A`;VAFF_)2oT zjwqQ}usP*e{`?h$4a7@tCN{M4V^Jr)D<{?V)!h;iCt0aN-jJC)1R^{R? z9Br-gc92xhBvl<>d}UD_XRX!sW{_UbWNC`kRjqdjh3y?WLw-GDGP37a)5WR6_<0$x zB~|r|vd??LxkV|$ncNu9?P=w>U&1492-mhnq<2qVRG*ZU>oAl_< z?a<=x(E?pLZ_QnFYs{wjo|I>G6Xn}vb{R8>@_ZBSnYy`CH`a?}a^fncBQMLb-8& zZe#n0=bK-o{Im^s>F_gc`{DjYTK#-;(HKwFS#wJ$cVdfLMColh_EDbSnA@tiasCe_ z+;5O0y_0BzBw1s+F6AZ7MGbupO}N~Xa$Y@W)YV(*g9lU2ZpS^A>e?&Ao^ntV-58&H z!*&|w-aWb90wXpT`%<1iNMt(h*^VU(<@zkq?jG~X`s02`T0WR@l~wYBKBiH=$5Iq8 znK1UkNy=vofIB*CmYdqCC!$yZ@Ah&h}`^*=@M1%(Ex_SV?)73b)`}w2SX{$}>zv8G!@4y~6#M zv^qh(xQlD=iG43q4v#qkBhFCC1353_>@JwLX-5U+Nxem0wcX3de5Kr}EjQ`hDw7ti z@Oq6L_xo@U#&7n!&89p^n>)ILW5%xjl#d+CeLeD=UhD|UEiFYur@mShGL>>j<;=bE z(Imi+@_co!)2y#G5iyhto^eh(7}X@Mr94Yn)J$ckMfzUKgIbC_12>;5$)G%`C0CL? zI;qP|%F~p%fjet2`#h&Ss;elY^P}mPYUJr!qBke4-Hlc7`j#BLHlmX8%Zj(Or@Tai z`@y5}aVIl*x|*m@zQpw~kMjIhBE9d8Q6nc(?m2{eBY(2_M=#2?I*86@$HgC*FXIBc zDWd+bRZ{$Me<&@_zJus_j`L*RZpz^XCnc$>h<%Q79*Y~_*5ADK4a(Wo9PON$E*j4% zcdO+LJ3M{5UM=NGDk5Qgi&17R$n`%SX=>cYT-9C^I#OPuCt8-!QguZi%8fg7o9)Vb zn>$>t&*CQU`C6nug>rb#*;zijBEnCe-b^%Z>Gigy0?G?DMM-Xw;F;?w&#L1b>-V%- z`9aE)B%Gd0hdx|zk#fNsj@@ZiZMS=rv+IT8=AAqqyrw*;v#4_O{QeuiQLfuoG;3jd zU#~WJKZG3i8r;{r>vcyMQhuAqo$;aVu>Qj+_txZ|W>&s7ccR=vP2}zGWiW(KIlL0u zj=A>Lc^>6oT8IqK-HMD}LwQiGup(*CuH$sS zqq|Ih*P$}Xc~?0))7(FFY@}T9L8xMVw$s+u9aQ5vS}-A$f;p5rHowA$R8^3*C$-i0>i_Xo?60UEzf^uFtC%1*?C(&BUqd>H@_G+HhcFG}DXfdSgmTL#)`6Zlh zaSeV`&QPA)C`|Axv-?QyXObi9F{iD4NUyawDQ7q57S0^fV^|^O`HjNbyZ8E4mQsGC zBX{4y=dOEke=p5np)9H{_;K9xE9Dj~MWcH>GPaQ3Z$%BKkDTe#wewjz`sgDA)K=#AOIW1DR#FR9>I_H|shewJrJTOJ)3aWeP@eUcWBK@7cc=H1C$YGnZ&}7>H&7ncQM6{&?6_en zct4&TDQ4U*2ciaE)TSKwuhzv2lO4KJKA;!3hfega;$D=0Y0rJ_9v(2ylJea4A|u@; zU%T2-ZlT0g+IQ0Nwln4SI->O3U4oa*q8!hEZ5O`e+xb$S--g?|r1OAQQIvn^%e9(Q z|KfQpQ`<4|@j^iO(&!g*x=ajSSgm=uX4j!+hJk>zdJx(;Lpn-CWwxXVA z`>Ov?!RImL7}QUsvG>FKe!4O)cpfCmcr@l)P&b-x-(9rp&4FPV=9H)Q6nVDuIH}5^ zJiiOqsIk;@wgcr*By7E7t?QLZlowivGUJ}@>gz#yZg=jGLqiAe52oCW&HWM9rH5gR zoVOKO7B=@kmPmPty6Cc1a7W%|%0p~L?6g+{p6;idt;$^=P$`Z%L%G0;TXR6?ve{)h zui|un@!s}TKILicL>C5VkJ6kgHDg@`%<3TgZnNsmT6=thel5Ocy~9| zaq|41gsg)7HtZR4t|4j*o!?u}rCgvbGVH(7(?5#x5(_SHvd`>VPC3s^qU9^bCePc+!5%DHba2dlm&Nk@ z<(%M@?I-GzD33A`-5Msl{$D|_gP*3!t_PEqPTCu{X^?` z*?Dxnv+R6gIa{`_>YbCVFWu=pGcuKpHpu0$G~uUoU4>6fODHBZ(M@T>w^M$W;zf#i z6w4{rP=q70vO7_f*TaDFK@^=RPHUp)L3sehs3z%4DBn!+B*jYtOkCl#uHOKJ^jK% zy)gowks&_#*&jSyu|Xc;_|`U%midzii3P}_wtPQ+7=Jh_`=C=M&=0Br*x~nkLjEoW z{0!C@_s}pq@?j!kiF`*)*6hD4kKwyV{Khm)@Z zx}ZOTe9Zhq%_4mQ%+McZ-hB5UGrzuOea$S)$;#9u8C??L9>)I%4f;u!f6$P6`5)8C zN7ob;M%#@aXE&yQKWUBN9`Llr;g2w1s*EVlRZ=cn3_Shug!c(72q@1rZC_Se%7!$p zm&&%eO3lhjCxAf94$~cpj2`#x+LP4ggiiy3z5&`p+n}gt$>QnvW}&P_k(%6kAC^FCEb5sMIkAC74o1CR zP4vuBZw%@gE9hk?@!0#6Fkh=8t#PC_xyov!{Iytqd(?F!@l++(J&d-U zaXiV}6psBch3K{-HO*B~MSJb3z6QqI65FItRLZbSfvP(2pe2SEi_1zifv2iQ;~KI$ zD3Q7iLcP8yA3)OvBDcVJf69%JcgJ{7%CXL>oiWa)T+wa|EJK;fZ2{EUU>x57hCUdq zsk|F*pxa}-6Nau-))w>W(0DW4p_2N}Aa&GolJ@VdxiKp;8~aePYHcoFg$;WVOa@omRK~7zI+UB{r;3T z7TYMV_dB#Rv#A}WzwCTzYDXcBjvr~?S&+KnSjbgZEej-Z?eXQgt<=l@dDX{qD* z!M`yKL&!Ly{vC~S4dh)gj3jc+F}dnEM~j;A*!j&s3CDJUEspIhwhTWj;atVC$RJcz zFDuniDjEJ#y;!Sob)lwoM)xbrGWms;mij=J8u7g$26GI9Fbv1wiopv5>GO%eQzK)- z7y}tIqcDueK+@t!``S*@BxAOiCD6ZxbNB(7$I`igvlb^qB+7kJ7EI(_W?}90wTS*x zlDD~CzOr&zX)eyGourLQHc}sYv3Ob|iN30Rt_t?J6VavX8m?*5xiE~@As*KRPt3c3 zX~#<@U?Iy#i(X*Et-W*S*971pJ*ge&+W7V8v@Hpzdau9}$ZGPWIm z-9gr_T;hv0*mfte%U>R8UPyZT|H0o>VoYtR`+qPv{sRNPIIi{#%sGvU;*9x+JIb)bi{f6fe(m&F& z6m58;t_JG3WALQ96H!LiK%9F&$I)zRa~jIrsNSD!22z{2#)CiAok?v@LOz|!oT*LH z*N#;7C!70mjN7439ELR*c463yfsBbU)HX?%kCR=Hr9S}uiDM785WkW!a<@x)ZihBy z+i^~DQP&>B0t~Sj5;1JRuoVNbIUCnFveuF{j?5)_o86koXJH!A?|^mC!nhs=eJV4< zKGh~=)BYm!g7{j#p0&d~t&nSDAo+-$-^u*v$HV^Ri(I;|}*mT9%kLill3g&(*>;a2Cq&1xINe$bKbPMWsw}ESoBfYeieQ zztJaav2?%H#yLNUEZ|tDnP`v9HR<`D+Uu!cqnAQlLm@s%A^xW^=#93?o?|u!`P_8* z5AsD~J|D~zrjXAMWyI%1F1Lk$s6)EerRxOr_^y36O(>d$8%J! zs&pI(oA9rY#(t^(Sau%ERh8~Na389siZzv=m&jhp3Bwc&()q=5Gkw{tsQK^$8^f}! zVKjpKSKNPUk}=qJlC*qHJaa9?u}#)8k}nM7WDOy4A;y=`ct7-~0k#wODnIv_COAeq zBPVj)Q|B+wRhI5ewqO}kNf|x)clY4gu8S_7S8z@LM)c?&a}TZ&WZuuf;DaF;Lo^0b z9$65Kj3~ynug~>wYC^1YXC_jbs=jy$ybMCpp2A3 z&q*7w-lU8{7(6h9U?A;^$B=@7^>-ZxqO$?N0YS>L#z4*; z*f-L4leVqIAh%ToJasar$=-MuhEo`>V0eJx35GHZ?=cXYAF!^^NL}e33xCS>wIUb_a~duwTzbA9m`eB$EJDPkvXX{0(TarrGL`tfq6;)l4}4` z?`{}K{Yr4$2VqN#iO=XB%7caW+D*;6!GB)avbAjB> zP&{9e@`yi4SvCr5M}JHs=Pfd}NgAGOrMw^IL(xa%+Jy9-Jii|5kaHxFlRTXi>|xoe z9Z^o&M$(#cV|uP6I%F?Qbcnsy82_C;9ZVzV);3i4Ke4CI5@-pq{VmAaK>e~@!IvJ? zZ|({{!gGPtzho_!=OKDn9?rkNWIU`787o$_KGN$Wy1&D9Ta}Et!8C0ca(#>sp`3?p zC%iufdHxQlBOgmLe_@|fdj^0B{ABGSHaekA(mx$2C+#7ew3DO{A%54HkgtvB zpTP>_q{koPenieH!aYZ`G83FD^1dhY*ofNfg!RzGIGIQCa`9S0DkJlrlq=n9|1a42 zooye5I`zUjk+DkrPR2M{4@r4s&1s2o(r(2yr>V?@_=Da%>5O?=pgyrj=7Zc0k&*c& zw<9EZI?v4SgjaLYy%QZDiu)_Y_$pFgrTcv(fggO z52+_zccuMK{o_vLH14Vpx5NF0DYd7#c1i6}eZ{zRe@x|AAJyhGFJ1#k>qqMKpTx0! zswy-uUYAMzMDi-i740aNr5IOJXv;$qr{^W<`G@X%+7mg>TXKKWg!o_KUZi4vvIda3 zBwu5+NItq|D#m+aUl2VqR^{uDv^;vhvp}IvT4_uxZ<As$pA3|)>`-)OIU5DDxw&5M9QoKF*f8=Pf?V1uPnEZol0$AoNf#pf52;^y6L_y&g zKf!iFpAv^11#UYv$BeOMJK5TplV^8Lp4SC=%vQ+sf2057JTTu)_Pw>m6vyBuh4kZw zCUPg-*bw<{DnCXspUO{Cet}{cm5V9ANznpN+{BKCyR6&}6utFj@_v-tQ54`wnCQ=- zJdk1qUf2`)GRl)Fe#4VEk?*Jc1VxW7GI`6{vU1x|wCy32hfy9&5pP^d?cApP8O0*J z;Y#eA%#qm{OfjXeOit3s5p+P7E`JW5W-CjVrK(G0vkQOaD--;m`xhs6$x$W1eurvA z>}OCurUcl3poNJ21qO)NKQI6h`vYtd(f=?J5&aDwi0EHffQbHtBt-Nd>_J3-K?WlF z2W}#U;TxT0h(i1l#y3PQYXHsTO62cGKqtho;rPk~VmcxRQQsC{vqePz!3@Ovh~9{T zkpNMM6CD5&5i<}|5w%7E?&(Uv0I?^cJ>nje$080yo`HB8c?C_^94*slBW9s|4CQVV z!w@@Tz9hu7=F=PtupW6D;$lQL-Z&{jc^zUUViNwr_HT&#PWTo+ zrfWI_tU^ph)J(_rBkCiTAsQgojmP#QMoj?xDo_dBM07%KfH!unk$WJ{KpuqXfmq^# zV-8VyF5q=l3AiEplYEGADDUlyuOA{NBBmi$A|62;gYQ$|mr5WNu^VCr#X^c-DC+y; z^H@xufvATVL@}0PD#as+M6a51V?d5EN*TM6w@i*M$(+`+4|!7ox#J9A_u+Js1JbXGGy*03M?E zG5|M3`*@tki0O!Nh(QTBZV-DX;H%90|^6#QTWeh?Xptw4U~e3y`}t zV?i8ZtTO&VQA7h3792nfYrz5`;x|>9zNQ-foRg$$u%HOh9)IpxfvDVyh2N{fa$Dn@ zVQ0{vS~C5chz7{Zv{_(*sH)2X4&t7+ESQ3Lza2}uF4Q3szbWgpzz^k#9k4!#%AHwo zgzzpb$U)R(W4VZ7h}DS3-SCWf7Teby%SB8@G(fCFv_zcIg9T#{wG3D=1JSrA3%n6y z5f>nqAd>Y!(@<7^C&VNo?}dMe7*P{2$8djC0(CVDb^vzk$frWAH*Z6IF5+k7VJkvziqN{ zmxx%0Tr&;(>pc2#50;NOWgqqvqVWMN2T}D9OL~50BSs;wK8p4c?bFdN;?|RB7qRX% z`VTQZ1MOVE_6o7z5szHN{zk0K!v01yxQycuF#|CUF(rouX^5Ivu-_145pxmYDhuy7 zmVgCf5lO#>^+nu+2tr&pb7ko>5V@$Ail~qBIz$7+1=n%DA*$xF@ZK7(--r{DXWhbi zfar7&`wubyKGp-#_#yfWG4&Dtj2lrkVRD6|Oe~t8v_1RDuH^vE7J?H8|f8qiS*d5UfMLBbFe}Ky<6eb|Y3J zMj^(2!geECe#UkqZbdwT*y;ai=l)%r2*Uu5zn(3gKZ%$jVv8phjTrRQs;5RsHI=BT z1}PGuLWT|*96DskkRgLZg$@xqWKehxlIsZy9TYMYth!|Akij8ChKx6K$k3re6~Awi ze?V|dpnUqg_vC%wd+yD9S6aHudc`6((fq}F#SPp>>o?aK+j!(F>);RPjq4c0)&2B| zOJl5Y+`|kmkFzJCbAYvh9V}zo?w|6a)P;F2UoE$N$t@;&058Ii#38<_!FnkFcwanWo=;-&2NTE zOwoo7wDI#f+OT+@KG2RbwrwTIzl4o8w)ED9nY!er680r)8)J^U^#9e$toOcZM0YkT_ z4ec#_OPsqsI3A#WPBrfs>OIzQ76aVVC~1S>}dG)cZjR`~71;dkc4P@qy9*d8Ufy585z{@kd7g`LTj?dR%;nQ;!FA z%A-Y|k2Y>&0%PgHJeTnW`CZg?R&z%G{u@Ibr-!~8wpWL71N-+soFp$8#(P6UPX_HV z)OiWi{pGO#+&qkHsPFeJ)box!9h_$xGt9G%3-~(2b;9m5YKPV5)D}mtQ3Xa7_=gJQ z*at}O)`NPlzFSwG>4m(AXL(W2_8c$aWxTAH^OSG;A)mx0o++TBzZag{!C6bKCH$1n z|CdlXVkM@8BqEkX#TG{rl9G&MB`0}VlA@GlSt?Q$5ntA&A)C^amb9fSf%Ifolxw;n aH{x1u)U{p5O}HsH>*ihE_2~6~EASU*`$Q=K literal 0 HcmV?d00001 diff --git a/module/incision/src/main/resources/native/windows/arm64/incision-jvmti.pdb b/module/incision/src/main/resources/native/windows/arm64/incision-jvmti.pdb new file mode 100644 index 0000000000000000000000000000000000000000..b35a1ec676461d9aea32f7f5f55c359765ce8289 GIT binary patch literal 1228800 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%0A)Ire5?J#JBJ|w=h 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 z%0D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 VbM|Uz53q|b3L>AY@AhWsV>}^ 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 z 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