From 5b68095f986b7cb6f174109b1137f3de0bb520a7 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 15:17:49 -0800 Subject: [PATCH 01/17] printmappings in testObf tasks --- enigma/build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/enigma/build.gradle b/enigma/build.gradle index 84a94cd14..0a30e2c96 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -131,6 +131,10 @@ def registerTestJarTasks(String name, String... input) { configuration confFileArg + final outDir = project.layout.buildDirectory.map { it.dir('test-obf') } + + printmapping(outDir.map { it.dir("${name}.map") }) + libraryjars( [jarfilter: '!**.jar', filter: '!module-info.class'], "${System.getProperty('java.home')}/jmods/java.base.jmod" @@ -138,7 +142,7 @@ def registerTestJarTasks(String name, String... input) { injars testJar.map { it.archiveFile } - outjars file("build/test-obf/${name}.jar") + outjars outDir.map { it.dir("${name}.jar") } } test.dependsOn(testObf) From 01ac31bffebb559a06061fb783b320ad06cd4f5c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 16:31:04 -0800 Subject: [PATCH 02/17] make :enigma-cli:test depend on test input obf tasks make SearchMappingsTest use its own, islolated jar --- enigma-cli/build.gradle | 2 ++ .../enigma/command/SearchMappingsTest.java | 3 ++- .../OtherReturnInterface.mapping | 2 +- .../search_mappings/OuterClass.mapping | 4 ++-- .../search_mappings/ParamType.mapping | 2 +- .../search_mappings/SelfReturnEnum.mapping | 6 ++--- .../search_mappings/{I.mapping => a.mapping} | 2 +- .../search_mappings/{ac.mapping => b.mapping} | 2 +- .../test/resources/search_mappings/c.mapping | 3 +++ .../search_mappings/{V.mapping => d.mapping} | 2 +- .../search_mappings/{ak.mapping => e.mapping} | 2 +- .../test/resources/search_mappings/r.mapping | 3 --- .../some/packaged/PackagedClass.mapping | 2 +- enigma/build.gradle | 13 ++++++++--- .../enigma/input/search_mappings/Obf_a.java | 13 +++++++++++ .../enigma/input/search_mappings/Obf_b.java | 21 +++++++++++++++++ .../enigma/input/search_mappings/Obf_c.java | 12 ++++++++++ .../enigma/input/search_mappings/Obf_d.java | 13 +++++++++++ .../enigma/input/search_mappings/Obf_e.java | 23 +++++++++++++++++++ .../search_mappings/OtherReturnInterface.java | 5 ++++ .../input/search_mappings/OuterClass.java | 14 +++++++++++ .../input/search_mappings/ParamType.java | 7 ++++++ .../input/search_mappings/SelfReturnEnum.java | 14 +++++++++++ .../packages/a/b/c/PackagedClass.java | 4 ++++ 24 files changed, 155 insertions(+), 19 deletions(-) rename enigma-cli/src/test/resources/search_mappings/{I.mapping => a.mapping} (85%) rename enigma-cli/src/test/resources/search_mappings/{ac.mapping => b.mapping} (95%) create mode 100644 enigma-cli/src/test/resources/search_mappings/c.mapping rename enigma-cli/src/test/resources/search_mappings/{V.mapping => d.mapping} (97%) rename enigma-cli/src/test/resources/search_mappings/{ak.mapping => e.mapping} (72%) delete mode 100644 enigma-cli/src/test/resources/search_mappings/r.mapping create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_a.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_b.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_c.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_d.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_e.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OtherReturnInterface.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OuterClass.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/ParamType.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/SelfReturnEnum.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/packages/a/b/c/PackagedClass.java diff --git a/enigma-cli/build.gradle b/enigma-cli/build.gradle index c9ef6b7e0..3d18f73ca 100644 --- a/enigma-cli/build.gradle +++ b/enigma-cli/build.gradle @@ -15,6 +15,8 @@ application { jar.manifest.attributes 'Main-Class': mainClass } +test.dependsOn(project(':enigma').tasks.named('obfuscateAllTestInput')) + publishing { publications { "$project.name"(MavenPublication) { diff --git a/enigma-cli/src/test/java/org/quiltmc/enigma/command/SearchMappingsTest.java b/enigma-cli/src/test/java/org/quiltmc/enigma/command/SearchMappingsTest.java index 85b9f17dc..c0b5597cb 100644 --- a/enigma-cli/src/test/java/org/quiltmc/enigma/command/SearchMappingsTest.java +++ b/enigma-cli/src/test/java/org/quiltmc/enigma/command/SearchMappingsTest.java @@ -22,7 +22,8 @@ import static org.quiltmc.enigma.TestUtil.getResource; public class SearchMappingsTest { - private static final Path JAR = TestUtil.obfJar("complete"); + // private static final Path JAR = TestUtil.obfJar("complete"); + private static final Path JAR = TestUtil.obfJar("search_mappings"); private static final Path MAPPINGS = getResource("/search_mappings"); // classes diff --git a/enigma-cli/src/test/resources/search_mappings/OtherReturnInterface.mapping b/enigma-cli/src/test/resources/search_mappings/OtherReturnInterface.mapping index 51f3729b3..e7843a910 100644 --- a/enigma-cli/src/test/resources/search_mappings/OtherReturnInterface.mapping +++ b/enigma-cli/src/test/resources/search_mappings/OtherReturnInterface.mapping @@ -1,3 +1,3 @@ -CLASS Z OtherReturnInterface +CLASS f OtherReturnInterface METHOD a abstractMethod (I)C ARG 1 intParam diff --git a/enigma-cli/src/test/resources/search_mappings/OuterClass.mapping b/enigma-cli/src/test/resources/search_mappings/OuterClass.mapping index cb35baadb..5a40beef7 100644 --- a/enigma-cli/src/test/resources/search_mappings/OuterClass.mapping +++ b/enigma-cli/src/test/resources/search_mappings/OuterClass.mapping @@ -1,3 +1,3 @@ -CLASS aa OuterClass +CLASS g OuterClass CLASS a InnerClass - METHOD a getOther ()LZ; + METHOD a getOther ()Lf; diff --git a/enigma-cli/src/test/resources/search_mappings/ParamType.mapping b/enigma-cli/src/test/resources/search_mappings/ParamType.mapping index c1e8e364d..99fe7c7b4 100644 --- a/enigma-cli/src/test/resources/search_mappings/ParamType.mapping +++ b/enigma-cli/src/test/resources/search_mappings/ParamType.mapping @@ -1 +1 @@ -CLASS t ParamType +CLASS h ParamType diff --git a/enigma-cli/src/test/resources/search_mappings/SelfReturnEnum.mapping b/enigma-cli/src/test/resources/search_mappings/SelfReturnEnum.mapping index cd80434e5..8a3eaf117 100644 --- a/enigma-cli/src/test/resources/search_mappings/SelfReturnEnum.mapping +++ b/enigma-cli/src/test/resources/search_mappings/SelfReturnEnum.mapping @@ -1,4 +1,4 @@ -CLASS l SelfReturnEnum - METHOD a staticGetArray ()[Ll; - METHOD a staticGet (Ljava/lang/String;)Ll; +CLASS i SelfReturnEnum + METHOD a staticGetArray ()[Li; + METHOD a staticGet (Ljava/lang/String;)Li; ARG 0 staticStringParam diff --git a/enigma-cli/src/test/resources/search_mappings/I.mapping b/enigma-cli/src/test/resources/search_mappings/a.mapping similarity index 85% rename from enigma-cli/src/test/resources/search_mappings/I.mapping rename to enigma-cli/src/test/resources/search_mappings/a.mapping index 5fc1c32d6..43dd8364a 100644 --- a/enigma-cli/src/test/resources/search_mappings/I.mapping +++ b/enigma-cli/src/test/resources/search_mappings/a.mapping @@ -1,2 +1,2 @@ -CLASS I +CLASS a FIELD a privateStringField Ljava/lang/String; diff --git a/enigma-cli/src/test/resources/search_mappings/ac.mapping b/enigma-cli/src/test/resources/search_mappings/b.mapping similarity index 95% rename from enigma-cli/src/test/resources/search_mappings/ac.mapping rename to enigma-cli/src/test/resources/search_mappings/b.mapping index 5bf485fab..c2389b9c3 100644 --- a/enigma-cli/src/test/resources/search_mappings/ac.mapping +++ b/enigma-cli/src/test/resources/search_mappings/b.mapping @@ -1,4 +1,4 @@ -CLASS ac +CLASS b FIELD a floatField F FIELD a intField I FIELD a stringField Ljava/lang/String; diff --git a/enigma-cli/src/test/resources/search_mappings/c.mapping b/enigma-cli/src/test/resources/search_mappings/c.mapping new file mode 100644 index 000000000..6d7a7f112 --- /dev/null +++ b/enigma-cli/src/test/resources/search_mappings/c.mapping @@ -0,0 +1,3 @@ +CLASS c + METHOD a staticVoidMethod (Lh;)V + ARG 0 staticTypedParam diff --git a/enigma-cli/src/test/resources/search_mappings/V.mapping b/enigma-cli/src/test/resources/search_mappings/d.mapping similarity index 97% rename from enigma-cli/src/test/resources/search_mappings/V.mapping rename to enigma-cli/src/test/resources/search_mappings/d.mapping index bea643236..38068f351 100644 --- a/enigma-cli/src/test/resources/search_mappings/V.mapping +++ b/enigma-cli/src/test/resources/search_mappings/d.mapping @@ -1,4 +1,4 @@ -CLASS V +CLASS d FIELD a recordInt I FIELD a recordString Ljava/lang/String; FIELD b PRIVATE_STATIC_FINAL_INT_FIELD I diff --git a/enigma-cli/src/test/resources/search_mappings/ak.mapping b/enigma-cli/src/test/resources/search_mappings/e.mapping similarity index 72% rename from enigma-cli/src/test/resources/search_mappings/ak.mapping rename to enigma-cli/src/test/resources/search_mappings/e.mapping index 265f7c1f5..41b394ab1 100644 --- a/enigma-cli/src/test/resources/search_mappings/ak.mapping +++ b/enigma-cli/src/test/resources/search_mappings/e.mapping @@ -1,2 +1,2 @@ -CLASS ak +CLASS e CLASS b InnerFieldType diff --git a/enigma-cli/src/test/resources/search_mappings/r.mapping b/enigma-cli/src/test/resources/search_mappings/r.mapping deleted file mode 100644 index d609e8edc..000000000 --- a/enigma-cli/src/test/resources/search_mappings/r.mapping +++ /dev/null @@ -1,3 +0,0 @@ -CLASS r - METHOD a staticVoidMethod (Lt;)V - ARG 0 staticTypedParam diff --git a/enigma-cli/src/test/resources/search_mappings/some/packaged/PackagedClass.mapping b/enigma-cli/src/test/resources/search_mappings/some/packaged/PackagedClass.mapping index ccf6e042e..2e55d1796 100644 --- a/enigma-cli/src/test/resources/search_mappings/some/packaged/PackagedClass.mapping +++ b/enigma-cli/src/test/resources/search_mappings/some/packaged/PackagedClass.mapping @@ -1 +1 @@ -CLASS Q some/packaged/PackagedClass +CLASS j some/packaged/PackagedClass diff --git a/enigma/build.gradle b/enigma/build.gradle index 0a30e2c96..2d9778fb0 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -108,12 +108,17 @@ static String taskNameFrom(String name) { return builder.toString() } + +tasks.register('obfuscateAllTestInput') { + group = 'test-setup' +} + // Generate obfuscated JARs for tests // If your test fails for class file version problem with proguard, run gradle with -Dorg.gradle.java.home="" flag def registerTestJarTasks(String name, String... input) { String taskName = taskNameFrom(name) final testJar = tasks.register("${taskName}TestJar", Jar.class) { - group = "test-setup" + group = 'test-setup' from(sourceSets.test.output) { include input } @@ -127,7 +132,7 @@ def registerTestJarTasks(String name, String... input) { : file('src/test/resources/proguard-test.conf') final testObf = tasks.register("${taskName}TestObf", ProGuardTask) { - group = "test-setup" + group = 'test-setup' configuration confFileArg @@ -145,7 +150,9 @@ def registerTestJarTasks(String name, String... input) { outjars outDir.map { it.dir("${name}.jar") } } - test.dependsOn(testObf) + tasks.named('obfuscateAllTestInput') { + dependsOn(testObf) + } } registerTestJarTasks("complete", "org/quiltmc/enigma/input/**/*.class") diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_a.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_a.java new file mode 100644 index 000000000..d05f47baa --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_a.java @@ -0,0 +1,13 @@ +package org.quiltmc.enigma.input.search_mappings; + +public class Obf_a { + private final String name; + + public Obf_a(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_b.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_b.java new file mode 100644 index 000000000..f42e21a68 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_b.java @@ -0,0 +1,21 @@ +package org.quiltmc.enigma.input.search_mappings; + +public class Obf_b { + public int one; + public float two; + public String three; + + public void m1() { + } + + public int m2() { + return 42; + } + + public void m3(int a1) { + } + + public int m4(int a1) { + return 5; // chosen by fair die roll, guaranteed to be random + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_c.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_c.java new file mode 100644 index 000000000..aa899cde4 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_c.java @@ -0,0 +1,12 @@ +package org.quiltmc.enigma.input.search_mappings; + +public class Obf_c { + public static void foo(final ParamType arg) { + System.out.println(new Object() { + @Override + public String toString() { + return arg.toString(); + } + }); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_d.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_d.java new file mode 100644 index 000000000..b31c8f3b9 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_d.java @@ -0,0 +1,13 @@ +package org.quiltmc.enigma.input.search_mappings; + +public record Obf_d(String a, String b, int c, double d) { + private static final int gaming = 234; + + public Obf_d(String a, String b, double d, int c) { + this(a, b, c, d); + } + + public Obf_d(String b) { + this("gaming", b, 1, 2.0); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_e.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_e.java new file mode 100644 index 000000000..d11a85313 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/Obf_e.java @@ -0,0 +1,23 @@ +package org.quiltmc.enigma.input.search_mappings; + +import java.util.List; +import java.util.Map; + +public class Obf_e { + public List f1; + public List f2; + public Map f3; + public B_Generic f5; + public B_Generic f6; + + public class A_Type { + } + + public class B_Generic { + public T f4; + + public T m1() { + return null; + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OtherReturnInterface.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OtherReturnInterface.java new file mode 100644 index 000000000..454b47ce3 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OtherReturnInterface.java @@ -0,0 +1,5 @@ +package org.quiltmc.enigma.input.search_mappings; + +public interface OtherReturnInterface { + char get(int n); +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OuterClass.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OuterClass.java new file mode 100644 index 000000000..0f7b4d28a --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/OuterClass.java @@ -0,0 +1,14 @@ +package org.quiltmc.enigma.input.search_mappings; + +public class OuterClass { + public static class Inner { + public OtherReturnInterface get() { + return new OtherReturnInterface() { + @Override + public char get(int n) { + return 0; + } + }; + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/ParamType.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/ParamType.java new file mode 100644 index 000000000..988962c38 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/ParamType.java @@ -0,0 +1,7 @@ +package org.quiltmc.enigma.input.search_mappings; + +public class ParamType { + class Inner { + // nothing to do + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/SelfReturnEnum.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/SelfReturnEnum.java new file mode 100644 index 000000000..eac65e4c9 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/SelfReturnEnum.java @@ -0,0 +1,14 @@ +package org.quiltmc.enigma.input.search_mappings; + +public enum SelfReturnEnum { + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/packages/a/b/c/PackagedClass.java b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/packages/a/b/c/PackagedClass.java new file mode 100644 index 000000000..154381684 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/search_mappings/packages/a/b/c/PackagedClass.java @@ -0,0 +1,4 @@ +package org.quiltmc.enigma.input.search_mappings.packages.a.b.c; + +public class PackagedClass { +} From f46fa7820e19a4e6a99199d235a8916d4d59bbe1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 16:34:09 -0800 Subject: [PATCH 03/17] remove z_ prefix from test input packages --- .../DropInvalidMappingsTestInput.java | 2 +- .../enigma/input/{z_tooltip => tooltip}/Constructors.java | 2 +- .../org/quiltmc/enigma/input/{z_tooltip => tooltip}/Enums.java | 2 +- .../org/quiltmc/enigma/input/{z_tooltip => tooltip}/Fields.java | 2 +- .../quiltmc/enigma/input/{z_tooltip => tooltip}/Lambdas.java | 2 +- .../quiltmc/enigma/input/{z_tooltip => tooltip}/Methods.java | 2 +- .../quiltmc/enigma/input/{z_tooltip => tooltip}/Records.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_drop_invalid_mappings => drop_invalid_mappings}/DropInvalidMappingsTestInput.java (69%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Constructors.java (88%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Enums.java (86%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Fields.java (90%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Lambdas.java (94%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Methods.java (88%) rename enigma/src/test/java/org/quiltmc/enigma/input/{z_tooltip => tooltip}/Records.java (92%) diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_drop_invalid_mappings/DropInvalidMappingsTestInput.java b/enigma/src/test/java/org/quiltmc/enigma/input/drop_invalid_mappings/DropInvalidMappingsTestInput.java similarity index 69% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_drop_invalid_mappings/DropInvalidMappingsTestInput.java rename to enigma/src/test/java/org/quiltmc/enigma/input/drop_invalid_mappings/DropInvalidMappingsTestInput.java index 70afb8ed9..b886d1d08 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_drop_invalid_mappings/DropInvalidMappingsTestInput.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/drop_invalid_mappings/DropInvalidMappingsTestInput.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_drop_invalid_mappings; +package org.quiltmc.enigma.input.drop_invalid_mappings; public class DropInvalidMappingsTestInput { static void staticMethod(int i, char c) { } diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java similarity index 88% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java index 4651b542f..7462fb0d7 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Constructors.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Constructors.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; public class Constructors { public Constructors(String outerArg) { diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java similarity index 86% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java index a35871652..8b89275ab 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Enums.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Enums.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; public enum Enums { FIRST, SECOND(2), diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java similarity index 90% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java index 18d8fb91d..d818046d6 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Fields.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Fields.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; public class Fields { static final String STATIC_FIELD = "static field"; diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java similarity index 94% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java index fd1f86b77..24f00e469 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Lambdas.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Lambdas.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; import java.util.ArrayList; import java.util.List; diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java similarity index 88% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java index 7547a0623..7dedc397f 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Methods.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Methods.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; public abstract class Methods { private void parameterized(int i, Boolean z, Methods methods) { } diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java similarity index 92% rename from enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java rename to enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java index 5164bae71..37a6ce35b 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/input/z_tooltip/Records.java +++ b/enigma/src/test/java/org/quiltmc/enigma/input/tooltip/Records.java @@ -1,4 +1,4 @@ -package org.quiltmc.enigma.input.z_tooltip; +package org.quiltmc.enigma.input.tooltip; public record Records() { public record WithStaticField(Boolean truth) { From b69f197d0a57c378346a3000acc3d2a5fc5195f4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 17:59:23 -0800 Subject: [PATCH 04/17] make :engima:test depend on :enigma:obfuscateAllTestInput --- enigma/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enigma/build.gradle b/enigma/build.gradle index 2d9778fb0..3204d2008 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -109,7 +109,7 @@ static String taskNameFrom(String name) { } -tasks.register('obfuscateAllTestInput') { +final obfuscateAllTestInput = tasks.register('obfuscateAllTestInput') { group = 'test-setup' } @@ -376,7 +376,7 @@ final testRecommendedImplPluginAgainstBumpedEnigma = tasks } } -test.dependsOn(testPreHandlingPluginAgainstCurrentEnigma, testRecommendedImplPluginAgainstBumpedEnigma) +test.dependsOn(obfuscateAllTestInput, testPreHandlingPluginAgainstCurrentEnigma, testRecommendedImplPluginAgainstBumpedEnigma) publishing { publications { From f230c658274561d4280f885156f0a192484d81c4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 18:06:25 -0800 Subject: [PATCH 05/17] update jar name in DropInvalidMappingsTest --- .../org/quiltmc/enigma/command/DropInvalidMappingsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma-cli/src/test/java/org/quiltmc/enigma/command/DropInvalidMappingsTest.java b/enigma-cli/src/test/java/org/quiltmc/enigma/command/DropInvalidMappingsTest.java index b8269fea3..3bcf5fe4d 100644 --- a/enigma-cli/src/test/java/org/quiltmc/enigma/command/DropInvalidMappingsTest.java +++ b/enigma-cli/src/test/java/org/quiltmc/enigma/command/DropInvalidMappingsTest.java @@ -13,7 +13,7 @@ public class DropInvalidMappingsTest extends CommandTest { private static final Path LONE_JAR = TestUtil.obfJar("lone_class"); private static final Path INNER_JAR = TestUtil.obfJar("inner_classes"); private static final Path ENUMS_JAR = TestUtil.obfJar("enums"); - private static final Path DROP_INVALID_MAPPINGS_JAR = TestUtil.obfJar("z_drop_invalid_mappings"); + private static final Path DROP_INVALID_MAPPINGS_JAR = TestUtil.obfJar("drop_invalid_mappings"); private static final Path INPUT_DIR = getResource("/drop_invalid_mappings/input/"); private static final Path EXPECTED_DIR = getResource("/drop_invalid_mappings/expected/"); From f9bd3cfc4b2dba61d8fa09777f9e029d951428c1 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 18:07:43 -0800 Subject: [PATCH 06/17] rename obfuscateAllTestInput -> obfuscateTestInputs --- enigma-cli/build.gradle | 2 +- enigma/build.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/enigma-cli/build.gradle b/enigma-cli/build.gradle index 3d18f73ca..b734618b3 100644 --- a/enigma-cli/build.gradle +++ b/enigma-cli/build.gradle @@ -15,7 +15,7 @@ application { jar.manifest.attributes 'Main-Class': mainClass } -test.dependsOn(project(':enigma').tasks.named('obfuscateAllTestInput')) +test.dependsOn(project(':enigma').tasks.named('obfuscateTestInputs')) publishing { publications { diff --git a/enigma/build.gradle b/enigma/build.gradle index 3204d2008..96e5aa98d 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -109,7 +109,7 @@ static String taskNameFrom(String name) { } -final obfuscateAllTestInput = tasks.register('obfuscateAllTestInput') { +final obfuscateTestInputs = tasks.register('obfuscateTestInputs') { group = 'test-setup' } @@ -150,7 +150,7 @@ def registerTestJarTasks(String name, String... input) { outjars outDir.map { it.dir("${name}.jar") } } - tasks.named('obfuscateAllTestInput') { + tasks.named('obfuscateTestInputs') { dependsOn(testObf) } } @@ -376,7 +376,7 @@ final testRecommendedImplPluginAgainstBumpedEnigma = tasks } } -test.dependsOn(obfuscateAllTestInput, testPreHandlingPluginAgainstCurrentEnigma, testRecommendedImplPluginAgainstBumpedEnigma) +test.dependsOn(obfuscateTestInputs, testPreHandlingPluginAgainstCurrentEnigma, testRecommendedImplPluginAgainstBumpedEnigma) publishing { publications { From 9fe0dbf12d276fbc5d2d22d540e7264f1f24f5bf Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 11:56:29 -0800 Subject: [PATCH 07/17] better utilize RecordIndexingVisitor in RecordComponentProposalService RecordIndexingVisitor: only check bytecode if multiple methods are getter candidates; allows finding overrides in simple cases RecordIndexingVisitor: when there are multiple getter candidates, check name and descriptor in addition to bytecode; prevents some false-positives in getter finding (though false positives for non-getters are still possible) --- .../RecordComponentProposalService.java | 55 ++---- .../impl/plugin/RecordIndexingVisitor.java | 174 ++++++++++++------ 2 files changed, 137 insertions(+), 92 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java index be55bc664..dddca4038 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java @@ -2,20 +2,17 @@ import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.api.Enigma; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.service.NameProposalService; import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; -import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; -import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeNode; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import java.util.HashMap; -import java.util.List; import java.util.Map; public record RecordComponentProposalService(RecordIndexingVisitor visitor) implements NameProposalService { @@ -29,14 +26,18 @@ public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex inde @Nullable @Override - public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + public Map, EntryMapping> getDynamicProposedNames( + EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, + @Nullable EntryMapping newMapping + ) { if (obfEntry instanceof FieldEntry fieldEntry) { - return this.mapRecordComponentGetter(remapper, fieldEntry.getContainingClass(), fieldEntry, newMapping); + return this.mapRecordComponentGetter(fieldEntry, newMapping); } else if (obfEntry == null) { - Map, EntryMapping> mappings = new HashMap<>(); - for (var mapping : remapper.getMappings()) { + final Map, EntryMapping> mappings = new HashMap<>(); + for (final EntryTreeNode mapping : remapper.getMappings()) { if (mapping.getEntry() instanceof FieldEntry fieldEntry) { - var getter = this.mapRecordComponentGetter(remapper, fieldEntry.getContainingClass(), fieldEntry, mapping.getValue()); + final Map, EntryMapping> getter = + this.mapRecordComponentGetter(fieldEntry, mapping.getValue()); if (getter != null) { mappings.putAll(getter); } @@ -50,34 +51,17 @@ public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remappe } @Nullable - private Map, EntryMapping> mapRecordComponentGetter(EntryRemapper remapper, ClassEntry parent, FieldEntry obfFieldEntry, EntryMapping mapping) { - EntryIndex entryIndex = remapper.getJarIndex().getIndex(EntryIndex.class); - ClassDefEntry parentDef = entryIndex.getDefinition(parent); - var def = entryIndex.getDefinition(obfFieldEntry); - if ((parentDef != null && !parentDef.isRecord()) || (def != null && def.getAccess().isStatic())) { - return null; - } - - List obfClassMethods = remapper.getJarIndex().getChildrenByClass().get(parentDef).stream() - .filter(e -> e instanceof MethodEntry) - .map(e -> (MethodEntry) e) - .toList(); - - MethodEntry obfMethodEntry = null; - for (MethodEntry method : obfClassMethods) { - if (this.isGetter(obfFieldEntry, method)) { - obfMethodEntry = method; - break; - } - } - - if (obfMethodEntry == null) { + private Map, EntryMapping> mapRecordComponentGetter(FieldEntry obfFieldEntry, EntryMapping mapping) { + final MethodEntry obfGetter = this.visitor.getComponentGetter(obfFieldEntry); + if (obfGetter == null) { return null; } // remap method to match field - EntryMapping newMapping = mapping.tokenType() == TokenType.OBFUSCATED ? new EntryMapping(null, null, TokenType.OBFUSCATED, null) : this.createMapping(mapping.targetName(), TokenType.DYNAMIC_PROPOSED); - return Map.of(obfMethodEntry, newMapping); + final EntryMapping getterMapping = mapping.tokenType() == TokenType.OBFUSCATED + ? EntryMapping.OBFUSCATED + : this.createMapping(mapping.targetName(), TokenType.DYNAMIC_PROPOSED); + return Map.of(obfGetter, getterMapping); } @Override @@ -89,11 +73,6 @@ public void validateProposedMapping(Entry entry, EntryMapping mapping, boolea NameProposalService.super.validateProposedMapping(entry, mapping, dynamic); } - public boolean isGetter(FieldEntry obfFieldEntry, MethodEntry method) { - final MethodEntry getter = this.visitor.getComponentGetter(obfFieldEntry); - return getter != null && getter.equals(method); - } - @Override public String getId() { return ID; diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 5c892eeda..7aedd79c4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -3,6 +3,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import org.jspecify.annotations.Nullable; import org.objectweb.asm.ClassVisitor; @@ -23,40 +24,74 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Stream; +// TODO add tests +// TODO javadoc, including getter uncertainty final class RecordIndexingVisitor extends ClassVisitor { + private static final int REQUIRED_GETTER_ACCESS = Opcodes.ACC_PUBLIC; + private static final int ILLEGAL_GETTER_ACCESS = Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_STATIC; + + private static final ImmutableSet ILLEGAL_GETTER_NAMES = ImmutableSet + .of("clone", "finalize", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait"); + + // visitation state fields; cleared in visitEnd() private ClassEntry clazz; private final Set recordComponents = new HashSet<>(); - private final Set fields = new HashSet<>(); - private final Set methods = new HashSet<>(); - - private final BiMap gettersByField; - private final Multimap fieldsByClass = HashMultimap.create(); - private final Multimap methodsByClass = HashMultimap.create(); + // this is a multimap because inner classes' fields go in the same map as their outer class's + private final Multimap fieldsByName = HashMultimap.create(); + private final Multimap methodsByDescriptor = HashMultimap.create(); + + // index fields; contents publicly queryable + private final Multimap componentFieldsByClass = HashMultimap.create(); + // holds methods that are at least probably getters for their field keys; superset of definiteComponentGettersByField + private final BiMap componentGettersByField = HashBiMap.create(); + // holds methods that are definitely the getters for their field keys + private final BiMap definiteComponentGettersByField = HashBiMap.create(); + // holds methods that are at least probably getters; superset of definiteComponentGettersByClass + private final Multimap componentGettersByClass = HashMultimap.create(); + // holds methods that are definitely component getters + private final Multimap definiteComponentGettersByClass = HashMultimap.create(); RecordIndexingVisitor() { super(Enigma.ASM_VERSION); - this.gettersByField = HashBiMap.create(); } @Nullable public MethodEntry getComponentGetter(FieldEntry componentField) { - return this.gettersByField.get(componentField); + return this.componentGettersByField.get(componentField); } @Nullable public FieldEntry getComponentField(MethodEntry componentGetter) { - return this.gettersByField.inverse().get(componentGetter); + return this.componentGettersByField.inverse().get(componentGetter); + } + + // TODO javadoc, prevent directly naming method (always match field) + @Nullable + public MethodEntry getDefiniteComponentGetter(FieldEntry componentField) { + return this.definiteComponentGettersByField.get(componentField); + } + + // TODO javadoc + @Nullable + public FieldEntry getDefiniteComponentField(MethodEntry componentGetter) { + return this.definiteComponentGettersByField.inverse().get(componentGetter); } public Stream streamComponentFields(ClassEntry recordEntry) { - return this.fieldsByClass.get(recordEntry).stream(); + return this.componentFieldsByClass.get(recordEntry).stream(); } public Stream streamComponentMethods(ClassEntry recordEntry) { - return this.methodsByClass.get(recordEntry).stream(); + return this.componentGettersByClass.get(recordEntry).stream(); + } + + // TODO javadoc + public Stream streamDefiniteComponentMethods(ClassEntry recordEntry) { + return this.definiteComponentGettersByClass.get(recordEntry).stream(); } @Override @@ -74,8 +109,8 @@ public RecordComponentVisitor visitRecordComponent(final String name, final Stri @Override public FieldVisitor visitField(final int access, final String name, final String descriptor, final String signature, final Object value) { if (this.clazz != null && ((access & Opcodes.ACC_PRIVATE) != 0) && this.recordComponents.stream().anyMatch(component -> component.name.equals(name))) { - FieldNode node = new FieldNode(this.api, access, name, descriptor, signature, value); - this.fields.add(node); + final FieldNode node = new FieldNode(this.api, access, name, descriptor, signature, value); + this.fieldsByName.put(node.name, node); return node; } @@ -85,8 +120,8 @@ public FieldVisitor visitField(final int access, final String name, final String @Override public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { if (this.clazz != null && ((access & Opcodes.ACC_PUBLIC) != 0)) { - MethodNode node = new MethodNode(this.api, access, name, descriptor, signature, exceptions); - this.methods.add(node); + final MethodNode node = new MethodNode(this.api, access, name, descriptor, signature, exceptions); + this.methodsByDescriptor.put(node.desc, node); return node; } @@ -103,8 +138,8 @@ public void visitEnd() { } finally { this.clazz = null; this.recordComponents.clear(); - this.fields.clear(); - this.methods.clear(); + this.fieldsByName.clear(); + this.methodsByDescriptor.clear(); } } @@ -113,43 +148,74 @@ private void collectResults() { return; } - for (RecordComponentNode component : this.recordComponents) { - FieldNode field = null; - for (FieldNode node : this.fields) { - if (node.name.equals(component.name) && node.desc.equals(component.descriptor)) { - field = node; - break; - } - } - - if (field == null) { - throw new RuntimeException("Field not found for record component: " + component.name); - } - - for (MethodNode method : this.methods) { - InsnList instructions = method.instructions; - - // match bytecode to exact expected bytecode for a getter - // only check important instructions (ignore new frame instructions, etc.) - if ( - instructions.size() == 6 - && instructions.get(2).getOpcode() == Opcodes.ALOAD - && instructions.get(3) instanceof FieldInsnNode fieldInsn - && fieldInsn.getOpcode() == Opcodes.GETFIELD - && fieldInsn.owner.equals(this.clazz.getFullName()) - && fieldInsn.desc.equals(field.desc) - && fieldInsn.name.equals(field.name) - && instructions.get(4).getOpcode() >= Opcodes.IRETURN - && instructions.get(4).getOpcode() <= Opcodes.ARETURN - ) { - final FieldEntry fieldEntry = new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)); - final MethodEntry methodEntry = new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc)); - - this.gettersByField.put(fieldEntry, methodEntry); - this.fieldsByClass.put(this.clazz, fieldEntry); - this.methodsByClass.put(this.clazz, methodEntry); - } - } + this.recordComponents.stream() + .map(component -> this.fieldsByName.get(component.name).stream() + .filter(field -> field.desc.equals(component.descriptor)) + .findAny() + .orElseThrow(() -> new IllegalStateException( + "Field not found for record component: " + component.name + )) + ) + .forEach(field -> { + final List potentialGetters = this.methodsByDescriptor + .get("()" + field.desc) + .stream() + .filter(method -> (method.access & REQUIRED_GETTER_ACCESS) == REQUIRED_GETTER_ACCESS) + .filter(method -> (method.access & ILLEGAL_GETTER_ACCESS) == 0) + .filter(method -> !ILLEGAL_GETTER_NAMES.contains(method.name)) + .toList(); + + if (potentialGetters.isEmpty()) { + throw new IllegalStateException("No potential getters for field: " + field); + } else { + final FieldEntry fieldEntry = + new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)); + // index the field even if a corresponding getter can't be found + this.componentFieldsByClass.put(this.clazz, fieldEntry); + + if (potentialGetters.size() == 1) { + this.indexGetter(potentialGetters.get(0), fieldEntry, true); + } else { + // If there are multiple methods with the getter's descriptor and access, it's impossible to + // tell which is the getter because obfuscation can mismatch getter/field names. + // This matching produces as few false-positives as possible by matching name, descriptor, + // and the bytecode of a default (non-overriden) getter method. + // It can still give a false-positive if a non-getter method's obfuscated name matches the + // field's, and that non-getter the has expected descriptor and bytecode of the getter. + // It also has false-negatives for getter overrides with non-default bytecode. + potentialGetters.stream() + .filter(method -> method.name.equals(field.name)) + // match bytecode to exact expected bytecode for a getter + // only check important instructions (ignore new frame instructions, etc.) + .filter(method -> { + final InsnList instructions = method.instructions; + return instructions.size() == 6 + && instructions.get(2).getOpcode() == Opcodes.ALOAD + && instructions.get(3) instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETFIELD + && fieldInsn.owner.equals(this.clazz.getFullName()) + && fieldInsn.desc.equals(field.desc) + && fieldInsn.name.equals(field.name) + && instructions.get(4).getOpcode() >= Opcodes.IRETURN + && instructions.get(4).getOpcode() <= Opcodes.ARETURN; + }) + .findAny() + .ifPresent(getter -> this.indexGetter(getter, fieldEntry, false)); + } + } + }); + } + + private void indexGetter(MethodNode getterNode, FieldEntry fieldEntry, boolean definite) { + final MethodEntry getterEntry = + new MethodEntry(this.clazz, getterNode.name, new MethodDescriptor(getterNode.desc)); + + this.componentGettersByField.put(fieldEntry, getterEntry); + this.componentGettersByClass.put(this.clazz, getterEntry); + + if (definite) { + this.definiteComponentGettersByField.put(fieldEntry, getterEntry); + this.definiteComponentGettersByClass.put(this.clazz, getterEntry); } } } From 93b9cea9a672a5706e9a77d9161f16862d56727c Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 12:11:20 -0800 Subject: [PATCH 08/17] expose probable component getter methods --- .../impl/plugin/RecordIndexingVisitor.java | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 7aedd79c4..a32c078f3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -46,27 +46,33 @@ final class RecordIndexingVisitor extends ClassVisitor { // index fields; contents publicly queryable private final Multimap componentFieldsByClass = HashMultimap.create(); - // holds methods that are at least probably getters for their field keys; superset of definiteComponentGettersByField - private final BiMap componentGettersByField = HashBiMap.create(); // holds methods that are definitely the getters for their field keys private final BiMap definiteComponentGettersByField = HashBiMap.create(); - // holds methods that are at least probably getters; superset of definiteComponentGettersByClass - private final Multimap componentGettersByClass = HashMultimap.create(); + // holds methods that are probably, but not certainly getters for their field keys + private final BiMap probableComponentGettersByField = HashBiMap.create(); // holds methods that are definitely component getters private final Multimap definiteComponentGettersByClass = HashMultimap.create(); + // holds methods that are probably, but not certainly getters + private final Multimap probableComponentGettersByClass = HashMultimap.create(); RecordIndexingVisitor() { super(Enigma.ASM_VERSION); } + // TODO javadoc @Nullable public MethodEntry getComponentGetter(FieldEntry componentField) { - return this.componentGettersByField.get(componentField); + final MethodEntry definiteGetter = this.definiteComponentGettersByField.get(componentField); + return definiteGetter == null ? this.probableComponentGettersByField.get(componentField) : definiteGetter; } + // TODO javadoc @Nullable public FieldEntry getComponentField(MethodEntry componentGetter) { - return this.componentGettersByField.inverse().get(componentGetter); + final FieldEntry definiteField = this.definiteComponentGettersByField.inverse().get(componentGetter); + return definiteField == null + ? this.probableComponentGettersByField.inverse().get(componentGetter) + : definiteField; } // TODO javadoc, prevent directly naming method (always match field) @@ -81,12 +87,29 @@ public FieldEntry getDefiniteComponentField(MethodEntry componentGetter) { return this.definiteComponentGettersByField.inverse().get(componentGetter); } + // TODO javadoc + @Nullable + public MethodEntry getProbableComponentGetter(FieldEntry componentField) { + return this.probableComponentGettersByField.get(componentField); + } + + // TODO javadoc + @Nullable + public FieldEntry getProbableComponentField(MethodEntry componentGetter) { + return this.probableComponentGettersByField.inverse().get(componentGetter); + } + + // TODO javadoc public Stream streamComponentFields(ClassEntry recordEntry) { return this.componentFieldsByClass.get(recordEntry).stream(); } + // TODO javadoc public Stream streamComponentMethods(ClassEntry recordEntry) { - return this.componentGettersByClass.get(recordEntry).stream(); + return Stream.concat( + this.definiteComponentGettersByClass.get(recordEntry).stream(), + this.probableComponentGettersByClass.get(recordEntry).stream() + ); } // TODO javadoc @@ -94,6 +117,11 @@ public Stream streamDefiniteComponentMethods(ClassEntry recordEntry return this.definiteComponentGettersByClass.get(recordEntry).stream(); } + // TODO javadoc + public Stream streamProbableComponentMethods(ClassEntry recordEntry) { + return this.probableComponentGettersByClass.get(recordEntry).stream(); + } + @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); @@ -210,12 +238,12 @@ private void indexGetter(MethodNode getterNode, FieldEntry fieldEntry, boolean d final MethodEntry getterEntry = new MethodEntry(this.clazz, getterNode.name, new MethodDescriptor(getterNode.desc)); - this.componentGettersByField.put(fieldEntry, getterEntry); - this.componentGettersByClass.put(this.clazz, getterEntry); - if (definite) { this.definiteComponentGettersByField.put(fieldEntry, getterEntry); this.definiteComponentGettersByClass.put(this.clazz, getterEntry); + } else { + this.probableComponentGettersByField.put(fieldEntry, getterEntry); + this.probableComponentGettersByClass.put(this.clazz, getterEntry); } } } From 9e920e97830d4ee8e0a4a65311334e453ad5d45a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 14:45:14 -0800 Subject: [PATCH 09/17] update record tests --- .../impl/plugin/RecordIndexingVisitor.java | 1 + .../input/records/NameMismatchRecord.java | 13 -- .../records/name_mismatch/BridgeRecord.java | 11 ++ .../FakeGetterRightInstructionsRecord.java | 17 +++ .../FakeGetterWrongInstructionsRecord.java | 17 +++ .../StringComponentOverrideGetterRecord.java | 15 ++ .../records/TestRecordComponentProposal.java | 141 +++++++++++++++--- 7 files changed, 183 insertions(+), 32 deletions(-) delete mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/BridgeRecord.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterRightInstructionsRecord.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterWrongInstructionsRecord.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/StringComponentOverrideGetterRecord.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index a32c078f3..425019d37 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -40,6 +40,7 @@ final class RecordIndexingVisitor extends ClassVisitor { // visitation state fields; cleared in visitEnd() private ClassEntry clazz; private final Set recordComponents = new HashSet<>(); + // TODO investigate this; may need to replace clazz with a class stack and to change this to fieldsByNameByClass // this is a multimap because inner classes' fields go in the same map as their outer class's private final Multimap fieldsByName = HashMultimap.create(); private final Multimap methodsByDescriptor = HashMultimap.create(); diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java deleted file mode 100644 index 6d3018a2d..000000000 --- a/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.quiltmc.enigma.input.records; - -public record NameMismatchRecord(int i) { - public int a() { - return 103; - } - - // obfuscates to b(), mismatching with the record component name - @Override - public int i() { - return this.i; - } -} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/BridgeRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/BridgeRecord.java new file mode 100644 index 000000000..713d44c95 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/BridgeRecord.java @@ -0,0 +1,11 @@ +package org.quiltmc.enigma.input.records.name_mismatch; + +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; + +import java.util.function.Supplier; + +/** + * {@link #get()} should be found by {@link RecordIndexingService} because it's the only getter candidate: the + * {@code Object get()} bridge method should not be a candidate because it has the wrong return type and wrong access. + */ +public record BridgeRecord(Double get) implements Supplier { } diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterRightInstructionsRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterRightInstructionsRecord.java new file mode 100644 index 000000000..f3a883e26 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterRightInstructionsRecord.java @@ -0,0 +1,17 @@ +package org.quiltmc.enigma.input.records.name_mismatch; + +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; + +public record FakeGetterRightInstructionsRecord(int component) { + /** + * This should be found by {@link RecordIndexingService} as the getter because it gets the same + * obf name as the component field and it has the expected descriptor, access, and instructions as a default getter. + * + *

This behavior is important because it matches decompilers' behavior. Decompilers will consider this a + * default getter and hide it, so it's important that we propose a name for it to prevent it from making stats + * un-completable. + */ + public int fakeGetter() { + return this.component; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterWrongInstructionsRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterWrongInstructionsRecord.java new file mode 100644 index 000000000..edce5ebe3 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/FakeGetterWrongInstructionsRecord.java @@ -0,0 +1,17 @@ +package org.quiltmc.enigma.input.records.name_mismatch; + +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; + +/** + * {@link #component()} shouldn't be found by {@link RecordIndexingService} - despite being a default getter - + * because its obf name doesn't match {@link #component}'s. + */ +public record FakeGetterWrongInstructionsRecord(int component) { + /** + * This shouldn't be found by {@link RecordIndexingService} - despite its obf name, access, and descriptor matching + * expectations for a getter - because its instructions don't match that of a default getter. + */ + public int fakeGetter() { + return 0; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/StringComponentOverrideGetterRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/StringComponentOverrideGetterRecord.java new file mode 100644 index 000000000..9e4ea7698 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/name_mismatch/StringComponentOverrideGetterRecord.java @@ -0,0 +1,15 @@ +package org.quiltmc.enigma.input.records.name_mismatch; + +import org.quiltmc.enigma.impl.plugin.RecordIndexingService; + +public record StringComponentOverrideGetterRecord(String string) { + /** + * This getter should be found by {@link RecordIndexingService} because it's the only getter candidate: + * the only other public no-args method returning a {@link String} is {@link #toString()}, and {@code toString} is + * no a legal component name. + */ + @Override + public String string() { + return ""; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java index 1fdb6ad4c..ddae78c80 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -15,12 +15,18 @@ import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.impl.plugin.RecordComponentProposalService; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.nio.file.Path; +/** + * Many record tests rely on the fact that proguard consistently names things in order a, b, c... which results in + * most default record component getters having the same name as their fields.
+ * Changing proguard's naming configs could break many tests. + */ public class TestRecordComponentProposal { private static final Path JAR = TestUtil.obfJar("records"); private static EnigmaProject project; @@ -71,32 +77,129 @@ void testSimpleRecordComponentProposal() { } @Test - void testMismatchRecordComponentProposal() { - // name of getter mismatches with name of field - ClassEntry cClass = TestEntryFactory.newClass("d"); - FieldEntry aField = TestEntryFactory.newField(cClass, "a", "I"); - MethodEntry fakeAGetter = TestEntryFactory.newMethod(cClass, "a", "()I"); - MethodEntry realAGetter = TestEntryFactory.newMethod(cClass, "b", "()I"); + void testFakeGetterWrongInstructions() { + final ClassEntry fakeGetterWrongInstructionsRecord = TestEntryFactory.newClass("h"); + final FieldEntry componentField = TestEntryFactory.newField(fakeGetterWrongInstructionsRecord, "a", "I"); + final MethodEntry fakeGetter = TestEntryFactory.newMethod(fakeGetterWrongInstructionsRecord, "a", "()I"); + final MethodEntry componentGetter = TestEntryFactory.newMethod(fakeGetterWrongInstructionsRecord, "b", "()I"); - Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aField).tokenType()); - Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(fakeAGetter).tokenType()); - Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(realAGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(componentField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(fakeGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(componentGetter).tokenType()); - project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); + final String targetName = "mapped"; + project.getRemapper().putMapping(TestUtil.newVC(), componentField, new EntryMapping(targetName)); - var fieldMapping = project.getRemapper().getMapping(aField); - Assertions.assertEquals(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); - Assertions.assertEquals("mapped", fieldMapping.targetName()); + final EntryMapping fieldMapping = project.getRemapper().getMapping(componentField); + Assertions.assertSame(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals(targetName, fieldMapping.targetName()); // fake getter should NOT be mapped - var fakeGetterMapping = project.getRemapper().getMapping(fakeAGetter); + final EntryMapping fakeGetterMapping = project.getRemapper().getMapping(fakeGetter); Assertions.assertEquals(TokenType.OBFUSCATED, fakeGetterMapping.tokenType()); - // real getter SHOULD be mapped - var realGetterMapping = project.getRemapper().getMapping(realAGetter); - Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, realGetterMapping.tokenType()); - Assertions.assertEquals("mapped", realGetterMapping.targetName()); - Assertions.assertEquals("enigma:record_component_proposer", realGetterMapping.sourcePluginId()); + // real getter should also NOT be mapped + // it's impossible to determine that it's the real getter + // this behavior matches decompilers' + final EntryMapping componentGetterMapping = project.getRemapper().getMapping(componentGetter); + Assertions.assertEquals(TokenType.OBFUSCATED, componentGetterMapping.tokenType()); + } + + @Test + void testFakeGetterRightInstructions() { + final ClassEntry fakeGetterRightInstructionsRecord = TestEntryFactory.newClass("g"); + final FieldEntry componentField = TestEntryFactory.newField(fakeGetterRightInstructionsRecord, "a", "I"); + final MethodEntry fakeGetter = TestEntryFactory.newMethod(fakeGetterRightInstructionsRecord, "a", "()I"); + final MethodEntry componentGetter = TestEntryFactory.newMethod(fakeGetterRightInstructionsRecord, "b", "()I"); + + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(componentField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(fakeGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(componentGetter).tokenType()); + + final String targetName = "mapped"; + project.getRemapper().putMapping(TestUtil.newVC(), componentField, new EntryMapping(targetName)); + + // FAKE getter SHOULD be mapped + // Assuming it's the getter - based on name, access, descriptor and instructions - matches decompilers' + // assumptions. + // Decompilers assume it's a default getter and hide it, so we propose a name to prevent un-completable stats. + final EntryMapping fakeGetterMappings = project.getRemapper().getMapping(fakeGetter); + Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, fakeGetterMappings.tokenType()); + Assertions.assertEquals(targetName, fakeGetterMappings.targetName()); + Assertions.assertEquals(RecordComponentProposalService.ID, fakeGetterMappings.sourcePluginId()); + + // real getter should NOT be mapped + final EntryMapping componentGetterMapping = project.getRemapper().getMapping(componentGetter); + Assertions.assertEquals(TokenType.OBFUSCATED, componentGetterMapping.tokenType()); + } + + @Test + void testBridgeRecord() { + final String doubleDesc = "Ljava/lang/Double;"; + final String stringGetterDesc = "()" + doubleDesc; + + final ClassEntry bridgeRecord = TestEntryFactory.newClass("f"); + final FieldEntry getField = TestEntryFactory.newField(bridgeRecord, "a", doubleDesc); + final MethodEntry getGetter = TestEntryFactory.newMethod(bridgeRecord, "a", stringGetterDesc); + final MethodEntry getBridge = TestEntryFactory.newMethod(bridgeRecord, "get", "()Ljava/lang/Object;"); + + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(getField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(getGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(getBridge).tokenType()); + + final String targetName = "mapped"; + project.getRemapper().putMapping(TestUtil.newVC(), getField, new EntryMapping(targetName)); + + final EntryMapping fieldMapping = project.getRemapper().getMapping(getField); + Assertions.assertSame(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals(targetName, fieldMapping.targetName()); + + // getter should be mapped; it should be the only getter candidate + final EntryMapping getterMapping = project.getRemapper().getMapping(getGetter); + Assertions.assertSame(TokenType.DYNAMIC_PROPOSED, getterMapping.tokenType()); + Assertions.assertEquals(targetName, getterMapping.targetName()); + Assertions.assertEquals(RecordComponentProposalService.ID, getterMapping.sourcePluginId()); + + // bridge should not be mapped; it should not be a getter candidate because + // it has the wrong access and descriptor + final EntryMapping bridgeMapping = project.getRemapper().getMapping(getBridge); + Assertions.assertEquals(TokenType.OBFUSCATED, bridgeMapping.tokenType()); + } + + @Test + void testIllegalGetterNameExclusion() { + final String stringDesc = "Ljava/lang/String;"; + final String stringGetterDesc = "()" + stringDesc; + + final ClassEntry stringComponentOverrideGetterRecord = TestEntryFactory.newClass("i"); + final FieldEntry stringField = TestEntryFactory.newField(stringComponentOverrideGetterRecord, "a", stringDesc); + final MethodEntry stringGetter = TestEntryFactory + .newMethod(stringComponentOverrideGetterRecord, "a", stringGetterDesc); + final MethodEntry toString = TestEntryFactory + .newMethod(stringComponentOverrideGetterRecord, "toString", stringGetterDesc); + + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(toString).tokenType()); + + final String targetName = "mapped"; + project.getRemapper().putMapping(TestUtil.newVC(), stringField, new EntryMapping(targetName)); + + final EntryMapping fieldMapping = project.getRemapper().getMapping(stringField); + Assertions.assertSame(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals(targetName, fieldMapping.targetName()); + + // getter should be mapped; it should be the only getter candidate: toString should be excluded from candidates + // because its name is not a legal component name + final EntryMapping getterMapping = project.getRemapper().getMapping(stringGetter); + Assertions.assertSame(TokenType.DYNAMIC_PROPOSED, getterMapping.tokenType()); + Assertions.assertEquals(targetName, getterMapping.targetName()); + Assertions.assertEquals(RecordComponentProposalService.ID, getterMapping.sourcePluginId()); + + // toString should not be mapped because it's name doesn't match the field, + // its name is no a legal component name, and it's a library method (unmappable) + final EntryMapping bridgeMapping = project.getRemapper().getMapping(toString); + Assertions.assertEquals(TokenType.OBFUSCATED, bridgeMapping.tokenType()); } @Test From 36617785a6a56a9a91cd2a162999b67dbbe4975d Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 19:33:19 -0800 Subject: [PATCH 10/17] update comments, javadoc RecordIndexingService --- .../impl/plugin/RecordIndexingService.java | 112 ++++++++++++++++++ .../impl/plugin/RecordIndexingVisitor.java | 48 +++++--- 2 files changed, 146 insertions(+), 14 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java index be2bbf042..845834310 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingService.java @@ -12,6 +12,34 @@ import java.util.Set; import java.util.stream.Stream; +/** + * Indexes records, finding component getters and their corresponding fields. + * + *

While component fields can be reliably indexed, there can be uncertainty in determining their corresponding + * getters. Some getters can be definitively determined, some are classified as 'probable getters' + * (probabilistically determined), and some cannot be determined at all. + * + *

{@link RecordIndexingService} provides separate methods for accessing getters that are definitive, probabilistic, + * or either.
+ * Either: + *

    + *
  • {@link #getComponentGetter(FieldEntry)} + *
  • {@link #getComponentField(MethodEntry)} + *
  • {@link #streamComponentMethods(ClassEntry)} + *
+ * Definite: + *
    + *
  • {@link #getDefiniteComponentGetter(FieldEntry)} + *
  • {@link #getDefiniteComponentField(MethodEntry)} + *
  • {@link #streamDefiniteComponentMethods(ClassEntry)} + *
+ * Probable: + *
    + *
  • {@link #getProbableComponentGetter(FieldEntry)} + *
  • {@link #getProbableComponentField(MethodEntry)} + *
  • {@link #streamProbableComponentMethods(ClassEntry)} + *
+ */ public class RecordIndexingService implements JarIndexerService { public static final String ID = "enigma:record_component_indexer"; @@ -21,24 +49,108 @@ public class RecordIndexingService implements JarIndexerService { this.visitor = visitor; } + /** + * @return the {@link MethodEntry} representing the getter of the passed {@code componentField}, + * or {@code null} if the passed {@code componentField} is not a record component field + * or if its getter could not be determined; returns both + * {@linkplain #getDefiniteComponentGetter(FieldEntry) definitive} and + * {@linkplain #getProbableComponentGetter(FieldEntry) probable} getters + */ @Nullable public MethodEntry getComponentGetter(FieldEntry componentField) { return this.visitor.getComponentGetter(componentField); } + /** + * @return the {@link FieldEntry} representing the field of the passed {@code componentGetter}, + * or {@code null} if the passed {@code componentGetter} is not a record component getter + * or if its field could not be determined; returns both + * {@linkplain #getDefiniteComponentField(MethodEntry) definitive} and + * {@linkplain #getProbableComponentField(MethodEntry) probable} fields + */ @Nullable public FieldEntry getComponentField(MethodEntry componentGetter) { return this.visitor.getComponentField(componentGetter); } + /** + * @return the definitive {@link MethodEntry} representing the getter of the passed {@code componentField}, + * or {@code null} if the passed {@code componentField} is not a record component field + * or if its getter could not be definitively determined + */ + @Nullable + public MethodEntry getDefiniteComponentGetter(FieldEntry componentField) { + return this.visitor.getDefiniteComponentGetter(componentField); + } + + /** + * @return the definitive {@link FieldEntry} representing the field of the passed {@code componentGetter}, + * or {@code null} if the passed {@code componentGetter} is not a record component getter + * or if its field could not be definitively determined + */ + @Nullable + public FieldEntry getDefiniteComponentField(MethodEntry componentGetter) { + return this.visitor.getDefiniteComponentField(componentGetter); + } + + /** + * @return the probable {@link MethodEntry} representing the getter of the passed {@code componentField}, + * or {@code null} if the passed {@code componentField} is not a record component field + * or if its getter was not probabilistically determined; + * does not include {@linkplain #getDefiniteComponentGetter(FieldEntry) definitive} getters + */ + @Nullable + public MethodEntry getProbableComponentGetter(FieldEntry componentField) { + return this.visitor.getProbableComponentGetter(componentField); + } + + /** + * @return the probably {@link FieldEntry} representing the field of the passed {@code componentGetter}, + * or {@code null} if the passed {@code componentGetter} is not a record component getter + * or if its field was not probabilistically determined; + * does not include {@linkplain #getDefiniteComponentField(MethodEntry) definitive} fields + */ + @Nullable + public FieldEntry getProbableComponentField(MethodEntry componentGetter) { + return this.visitor.getProbableComponentField(componentGetter); + } + + /** + * @return a {@link Stream} of component fields of the passed {@code recordEntry}; + * there's no uncertainty in getter field determination, so all fields are always included; + * if the passed {@code recordEntry} does not represent a record, the stream is empty + */ public Stream streamComponentFields(ClassEntry recordEntry) { return this.visitor.streamComponentFields(recordEntry); } + /** + * @return a {@link Stream} of component getter methods of the passed {@code recordEntry}; + * includes both {@linkplain #streamDefiniteComponentMethods(ClassEntry) definitive} and + * {@linkplain #streamProbableComponentMethods(ClassEntry) probable} getters; + * if the passed {@code recordEntry} does not represent a record, the stream is empty + */ public Stream streamComponentMethods(ClassEntry recordEntry) { return this.visitor.streamComponentMethods(recordEntry); } + /** + * @return a {@link Stream} of definitive component getter methods of the passed {@code recordEntry}; + * if the passed {@code recordEntry} does not represent a record, the stream is empty + */ + public Stream streamDefiniteComponentMethods(ClassEntry recordEntry) { + return this.visitor.streamDefiniteComponentMethods(recordEntry); + } + + /** + * @return a {@link Stream} of probable component getter methods of the passed {@code recordEntry}; + * does not include {@linkplain #streamDefiniteComponentMethods(ClassEntry) definitive} getters; + * if the passed {@code recordEntry} does not represent a record, the stream is empty + */ + public Stream streamProbableComponentMethods(ClassEntry recordEntry) { + return this.visitor.streamProbableComponentMethods(recordEntry); + } + @Override public void acceptJar(Set scope, ProjectClassProvider classProvider, JarIndex jarIndex) { for (String className : scope) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 425019d37..6d97ad63d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -28,8 +28,9 @@ import java.util.Set; import java.util.stream.Stream; -// TODO add tests -// TODO javadoc, including getter uncertainty +/** + * @see RecordIndexingService + */ final class RecordIndexingVisitor extends ClassVisitor { private static final int REQUIRED_GETTER_ACCESS = Opcodes.ACC_PUBLIC; private static final int ILLEGAL_GETTER_ACCESS = Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_STATIC; @@ -40,8 +41,7 @@ final class RecordIndexingVisitor extends ClassVisitor { // visitation state fields; cleared in visitEnd() private ClassEntry clazz; private final Set recordComponents = new HashSet<>(); - // TODO investigate this; may need to replace clazz with a class stack and to change this to fieldsByNameByClass - // this is a multimap because inner classes' fields go in the same map as their outer class's + // this is a multimap because proguard can give component fields with different types the same name private final Multimap fieldsByName = HashMultimap.create(); private final Multimap methodsByDescriptor = HashMultimap.create(); @@ -60,14 +60,18 @@ final class RecordIndexingVisitor extends ClassVisitor { super(Enigma.ASM_VERSION); } - // TODO javadoc + /** + * @see RecordIndexingService#getComponentGetter(FieldEntry) + */ @Nullable public MethodEntry getComponentGetter(FieldEntry componentField) { final MethodEntry definiteGetter = this.definiteComponentGettersByField.get(componentField); return definiteGetter == null ? this.probableComponentGettersByField.get(componentField) : definiteGetter; } - // TODO javadoc + /** + * @see RecordIndexingService#getComponentField(MethodEntry) + */ @Nullable public FieldEntry getComponentField(MethodEntry componentGetter) { final FieldEntry definiteField = this.definiteComponentGettersByField.inverse().get(componentGetter); @@ -76,36 +80,48 @@ public FieldEntry getComponentField(MethodEntry componentGetter) { : definiteField; } - // TODO javadoc, prevent directly naming method (always match field) + /** + * @see RecordIndexingService#getDefiniteComponentGetter(FieldEntry) + */ @Nullable public MethodEntry getDefiniteComponentGetter(FieldEntry componentField) { return this.definiteComponentGettersByField.get(componentField); } - // TODO javadoc + /** + * @see RecordIndexingService#getDefiniteComponentField(MethodEntry) + */ @Nullable public FieldEntry getDefiniteComponentField(MethodEntry componentGetter) { return this.definiteComponentGettersByField.inverse().get(componentGetter); } - // TODO javadoc + /** + * @see RecordIndexingService#getProbableComponentGetter(FieldEntry) + */ @Nullable public MethodEntry getProbableComponentGetter(FieldEntry componentField) { return this.probableComponentGettersByField.get(componentField); } - // TODO javadoc + /** + * @see RecordIndexingService#getProbableComponentField(MethodEntry) + */ @Nullable public FieldEntry getProbableComponentField(MethodEntry componentGetter) { return this.probableComponentGettersByField.inverse().get(componentGetter); } - // TODO javadoc + /** + * @see RecordIndexingService#streamComponentFields(ClassEntry) + */ public Stream streamComponentFields(ClassEntry recordEntry) { return this.componentFieldsByClass.get(recordEntry).stream(); } - // TODO javadoc + /** + * @see RecordIndexingService#streamComponentMethods(ClassEntry) + */ public Stream streamComponentMethods(ClassEntry recordEntry) { return Stream.concat( this.definiteComponentGettersByClass.get(recordEntry).stream(), @@ -113,12 +129,16 @@ public Stream streamComponentMethods(ClassEntry recordEntry) { ); } - // TODO javadoc + /** + * @see RecordIndexingService#streamDefiniteComponentMethods(ClassEntry) + */ public Stream streamDefiniteComponentMethods(ClassEntry recordEntry) { return this.definiteComponentGettersByClass.get(recordEntry).stream(); } - // TODO javadoc + /** + * @see RecordIndexingService#streamProbableComponentMethods(ClassEntry) + */ public Stream streamProbableComponentMethods(ClassEntry recordEntry) { return this.probableComponentGettersByClass.get(recordEntry).stream(); } From a5144db7d7af56d2247f038ebfa8ec7910492931 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Mon, 17 Nov 2025 19:37:20 -0800 Subject: [PATCH 11/17] checkstyle --- .../impl/plugin/RecordIndexingVisitor.java | 20 +++++++++---------- .../records/TestRecordComponentProposal.java | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java index 6d97ad63d..8e8e136bc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordIndexingVisitor.java @@ -76,8 +76,8 @@ public MethodEntry getComponentGetter(FieldEntry componentField) { public FieldEntry getComponentField(MethodEntry componentGetter) { final FieldEntry definiteField = this.definiteComponentGettersByField.inverse().get(componentGetter); return definiteField == null - ? this.probableComponentGettersByField.inverse().get(componentGetter) - : definiteField; + ? this.probableComponentGettersByField.inverse().get(componentGetter) + : definiteField; } /** @@ -239,14 +239,14 @@ private void collectResults() { .filter(method -> { final InsnList instructions = method.instructions; return instructions.size() == 6 - && instructions.get(2).getOpcode() == Opcodes.ALOAD - && instructions.get(3) instanceof FieldInsnNode fieldInsn - && fieldInsn.getOpcode() == Opcodes.GETFIELD - && fieldInsn.owner.equals(this.clazz.getFullName()) - && fieldInsn.desc.equals(field.desc) - && fieldInsn.name.equals(field.name) - && instructions.get(4).getOpcode() >= Opcodes.IRETURN - && instructions.get(4).getOpcode() <= Opcodes.ARETURN; + && instructions.get(2).getOpcode() == Opcodes.ALOAD + && instructions.get(3) instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETFIELD + && fieldInsn.owner.equals(this.clazz.getFullName()) + && fieldInsn.desc.equals(field.desc) + && fieldInsn.name.equals(field.name) + && instructions.get(4).getOpcode() >= Opcodes.IRETURN + && instructions.get(4).getOpcode() <= Opcodes.ARETURN; }) .findAny() .ifPresent(getter -> this.indexGetter(getter, fieldEntry, false)); diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java index ddae78c80..ccfe02f30 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -174,9 +174,9 @@ void testIllegalGetterNameExclusion() { final ClassEntry stringComponentOverrideGetterRecord = TestEntryFactory.newClass("i"); final FieldEntry stringField = TestEntryFactory.newField(stringComponentOverrideGetterRecord, "a", stringDesc); final MethodEntry stringGetter = TestEntryFactory - .newMethod(stringComponentOverrideGetterRecord, "a", stringGetterDesc); + .newMethod(stringComponentOverrideGetterRecord, "a", stringGetterDesc); final MethodEntry toString = TestEntryFactory - .newMethod(stringComponentOverrideGetterRecord, "toString", stringGetterDesc); + .newMethod(stringComponentOverrideGetterRecord, "toString", stringGetterDesc); Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringField).tokenType()); Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringGetter).tokenType()); From be470eafe55f9788e441106d97a30b81f4cd53b4 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 07:07:22 -0800 Subject: [PATCH 12/17] update comments --- .../quiltmc/enigma/records/TestRecordComponentProposal.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java index ccfe02f30..4dc983254 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -140,6 +140,7 @@ void testBridgeRecord() { final ClassEntry bridgeRecord = TestEntryFactory.newClass("f"); final FieldEntry getField = TestEntryFactory.newField(bridgeRecord, "a", doubleDesc); + // once Supplier is indexed as a lib, this should be named get final MethodEntry getGetter = TestEntryFactory.newMethod(bridgeRecord, "a", stringGetterDesc); final MethodEntry getBridge = TestEntryFactory.newMethod(bridgeRecord, "get", "()Ljava/lang/Object;"); @@ -197,7 +198,7 @@ void testIllegalGetterNameExclusion() { Assertions.assertEquals(RecordComponentProposalService.ID, getterMapping.sourcePluginId()); // toString should not be mapped because it's name doesn't match the field, - // its name is no a legal component name, and it's a library method (unmappable) + // its name is not a legal component name, and it's a library method (unmappable) final EntryMapping bridgeMapping = project.getRemapper().getMapping(toString); Assertions.assertEquals(TokenType.OBFUSCATED, bridgeMapping.tokenType()); } From a51de6c989a9523feaf6c5ff6f5f7288d8ee8f1a Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 8 Nov 2025 17:57:20 -0800 Subject: [PATCH 13/17] add ParameterConnections test input --- .../z_parameter_connection/ParameterConnections.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java new file mode 100644 index 000000000..868d1adf5 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java @@ -0,0 +1,12 @@ +package org.quiltmc.enigma.input.z_parameter_connection; + +public class ParameterConnections { + static Object toStringOf(String param) { + return new Object() { + @Override + public String toString() { + return param; + } + }; + } +} From b86ee832339ee7f5f838ab23019894e38d3ffdf5 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sat, 8 Nov 2025 19:34:42 -0800 Subject: [PATCH 14/17] disable user renaming of enum constants --- .../org/quiltmc/enigma/api/EnigmaProject.java | 40 +++++++++++++-- .../api/analysis/tree/StructureTreeNode.java | 4 +- .../api/source/DecompiledClassSource.java | 4 ++ .../enigma/impl/plugin/BuiltinPlugin.java | 32 ++---------- .../plugin/EnumConstantIndexingService.java | 44 +++++++++++++++++ .../plugin/EnumConstantProposalService.java | 49 +++++++++++++++++++ .../plugin/EnumFieldNameFindingVisitor.java | 25 +++++++--- 7 files changed, 155 insertions(+), 43 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java create mode 100644 enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java index 41473e185..4abe7b522 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -10,13 +10,13 @@ import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; +import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.service.ObfuscationTestService; import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.mapping.EntryResolver; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; -import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; import org.quiltmc.enigma.api.service.DecompilerService; @@ -32,6 +32,9 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; +import org.quiltmc.enigma.impl.plugin.EnumConstantIndexingService; import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker; import org.quiltmc.enigma.util.I18n; import org.tinylog.Logger; @@ -48,6 +51,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -199,7 +203,7 @@ public boolean isNavigable(Entry obfEntry) { return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); } - public boolean isRenamable(Entry obfEntry) { + public boolean isInternallyRenamable(Entry obfEntry) { if (obfEntry instanceof MethodEntry obfMethodEntry) { // constructors are not renamable! if (obfMethodEntry.isConstructor()) { @@ -227,9 +231,11 @@ public boolean isRenamable(Entry obfEntry) { if (this.isLibraryMethodOverride(obfMethodEntry)) { return false; } - } else if (obfEntry instanceof LocalVariableEntry localEntry && !localEntry.isArgument()) { - return false; - } else if (obfEntry instanceof LocalVariableEntry localEntry && localEntry.isArgument()) { + } else if (obfEntry instanceof LocalVariableEntry localEntry) { + if (!localEntry.isArgument()) { + return false; + } + MethodEntry method = localEntry.getParent(); ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(method.getParent()); @@ -274,10 +280,34 @@ private boolean isLibraryMethodOverride(MethodEntry methodEntry) { } } + public boolean isRenamable(Entry obfEntry) { + if (this.isInternallyRenamable(obfEntry)) { + if (obfEntry instanceof FieldEntry fieldEntry) { + return !this.getEnumConstantIndexingService() + .map(service -> service.isEnumConstant(fieldEntry)) + .orElse(false); + } else { + return true; + } + } else { + return false; + } + } + + private Optional getEnumConstantIndexingService() { + return this.getEnigma() + .getService(JarIndexerService.TYPE, EnumConstantIndexingService.ID) + .map(service -> (EnumConstantIndexingService) service); + } + private static boolean isEnumValueOfMethod(ClassDefEntry parent, MethodEntry method) { return parent != null && parent.isEnum() && method.getName().equals("valueOf") && method.getDesc().toString().equals("(Ljava/lang/String;)L" + parent.getFullName() + ";"); } + public boolean isInternallyRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && this.isInternallyRenamable(obfReference.getNameableEntry()); + } + public boolean isRenamable(EntryReference, Entry> obfReference) { return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java index 3a8b6f40d..7a0ef4c37 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java @@ -44,11 +44,11 @@ public void load(EnigmaProject project, StructureTreeOptions options) { case ALL -> children; case OBFUSCATED -> children // remove deobfuscated members if only obfuscated, unless it's an inner class - .filter(e -> (e instanceof ClassEntry) || (project.isObfuscated(e) && project.isRenamable(e))) + .filter(e -> (e instanceof ClassEntry) || (project.isObfuscated(e) && project.isInternallyRenamable(e))) // keep constructor methods if the class is obfuscated .filter(e -> !(e instanceof MethodEntry m && m.isConstructor()) || project.isObfuscated(e.getParent())); case DEOBFUSCATED -> children.filter(e -> (e instanceof ClassEntry) - || (!project.isObfuscated(e) && project.isRenamable(e)) + || (!project.isObfuscated(e) && project.isInternallyRenamable(e)) // keep constructor methods if the class is deobfuscated || (e instanceof MethodEntry m && m.isConstructor()) && !project.isObfuscated(e.getParent())); }; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java index e6a49631f..086884876 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java @@ -64,6 +64,10 @@ private String remapToken(TokenStore target, EnigmaProject project, Token token, } else { target.add(project, EntryMapping.OBFUSCATED, movedToken); } + } else if (project.isInternallyRenamable(reference)) { + if (translatedEntry != null && !translatedEntry.isObfuscated()) { + return translatedEntry.getValue().getSourceRemapName(); + } } else if (DEBUG_TOKEN_HIGHLIGHTS) { target.add(project, new EntryMapping(null, null, TokenType.DEBUG, null), movedToken); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java index 50d4eb16a..7485f0158 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -6,7 +6,6 @@ import org.quiltmc.enigma.api.EnigmaPlugin; import org.quiltmc.enigma.api.EnigmaPluginContext; import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.service.JarIndexerService; @@ -37,35 +36,10 @@ public boolean supportsEnigmaVersion(@NonNull Version enigmaVersion) { } private static void registerEnumNamingService(EnigmaPluginContext ctx) { - final Map, String> names = new HashMap<>(); - final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); + final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(); - ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:enum_initializer_indexer")); - - ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { - @Override - public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex index) { - Map, EntryMapping> mappings = new HashMap<>(); - - index.getIndex(EntryIndex.class).getFields().forEach(field -> { - if (names.containsKey(field)) { - mappings.put(field, this.createMapping(names.get(field), TokenType.JAR_PROPOSED)); - } - }); - - return mappings; - } - - @Override - public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { - return null; - } - - @Override - public String getId() { - return "enigma:enum_name_proposer"; - } - }); + ctx.registerService(JarIndexerService.TYPE, ctx1 -> new EnumConstantIndexingService(visitor)); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new EnumConstantProposalService(visitor)); } private static void registerRecordNamingService(EnigmaPluginContext ctx) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java new file mode 100644 index 000000000..d92445943 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java @@ -0,0 +1,44 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.objectweb.asm.tree.ClassNode; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; + +import javax.annotation.Nullable; +import java.util.Set; + +public class EnumConstantIndexingService implements JarIndexerService { + public static final String ID = "enigma:enum_initializer_indexer"; + + private final EnumFieldNameFindingVisitor visitor; + + EnumConstantIndexingService(EnumFieldNameFindingVisitor visitor) { + this.visitor = visitor; + } + + @Override + public void acceptJar(Set scope, ProjectClassProvider classProvider, JarIndex jarIndex) { + for (String className : scope) { + ClassNode node = classProvider.get(className); + if (node != null) { + node.accept(this.visitor); + } + } + } + + @Override + public String getId() { + return ID; + } + + public boolean isEnumConstant(FieldEntry field) { + return this.visitor.isEnumConstant(field); + } + + @Nullable + public String getEnumConstantName(FieldEntry field) { + return this.visitor.getEnumConstantName(field); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java new file mode 100644 index 000000000..67eb68acc --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java @@ -0,0 +1,49 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +public class EnumConstantProposalService implements NameProposalService { + private final EnumFieldNameFindingVisitor visitor; + + EnumConstantProposalService(EnumFieldNameFindingVisitor visitor) { + this.visitor = visitor; + } + + @Override + public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + + index.getIndex(EntryIndex.class).getFields().forEach(field -> { + final String name = this.visitor.getEnumConstantName(field); + if (name != null) { + mappings.put(field, this.createMapping(name, TokenType.JAR_PROPOSED)); + } + }); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames( + EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, + @Nullable EntryMapping newMapping + ) { + return null; + } + + @Override + public String getId() { + return "enigma:enum_name_proposer"; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java index a9e5baeba..fa706c276 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java @@ -18,10 +18,11 @@ import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.translation.representation.TypeDescriptor; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; -import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -30,13 +31,13 @@ final class EnumFieldNameFindingVisitor extends ClassVisitor { private ClassEntry clazz; private String className; - private final Map, String> mappings; + private final Map enumConstants; private final Set> enumFields = new HashSet<>(); private final List classInits = new ArrayList<>(); - EnumFieldNameFindingVisitor(Map, String> mappings) { + EnumFieldNameFindingVisitor() { super(Enigma.ASM_VERSION); - this.mappings = mappings; + this.enumConstants = new HashMap<>(); } @Override @@ -44,8 +45,6 @@ public void visit(int version, int access, String name, String signature, String super.visit(version, access, name, signature, superName, interfaces); this.className = name; this.clazz = new ClassEntry(name); - this.enumFields.clear(); - this.classInits.clear(); } @Override @@ -76,9 +75,21 @@ public void visitEnd() { this.collectResults(); } catch (Exception ex) { throw new RuntimeException(ex); + } finally { + this.enumFields.clear(); + this.classInits.clear(); } } + public boolean isEnumConstant(FieldEntry field) { + return this.enumConstants.containsKey(field); + } + + @Nullable + public String getEnumConstantName(FieldEntry field) { + return this.enumConstants.get(field); + } + private void collectResults() throws Exception { String owner = this.className; Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); @@ -108,7 +119,7 @@ private void collectResults() throws Exception { } if (s != null) { - this.mappings.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + this.enumConstants.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); } // report otherwise? From 576be2f0d9e389c9e66e34be1bad369caf4b5072 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Sun, 9 Nov 2025 09:34:28 -0800 Subject: [PATCH 15/17] extract EnigmaProjectImpl --- .../java/org/quiltmc/enigma/api/Enigma.java | 2 +- .../org/quiltmc/enigma/api/EnigmaProject.java | 499 ++--------------- .../api/analysis/tree/StructureTreeNode.java | 5 + .../api/source/DecompiledClassSource.java | 5 +- .../enigma/impl/EnigmaProjectImpl.java | 526 ++++++++++++++++++ 5 files changed, 580 insertions(+), 457 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java index d8c1c81b2..27fff7fed 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java @@ -165,7 +165,7 @@ public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, Prog MappingsIndex mappingsIndex = MappingsIndex.empty(); mappingsIndex.indexMappings(proposedNames, progress); - return new EnigmaProject(this, path, mainProjectProvider, jarIndex, libIndex, comboIndex, mappingsIndex, proposedNames, Utils.zipSha1(path)); + return EnigmaProject.of(this, path, mainProjectProvider, jarIndex, libIndex, comboIndex, mappingsIndex, proposedNames, Utils.zipSha1(path)); } private Predicate createMainReferencedPredicate(AbstractJarIndex mainIndex, ProjectClassProvider classProvider) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java index 4abe7b522..5448a4f39 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -1,88 +1,34 @@ package org.quiltmc.enigma.api; -import com.google.common.base.Functions; -import com.google.common.base.Preconditions; import org.jspecify.annotations.Nullable; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.tree.ClassNode; import org.quiltmc.enigma.api.analysis.EntryReference; -import org.quiltmc.enigma.api.analysis.index.jar.EnclosingMethodIndex; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; -import org.quiltmc.enigma.api.service.JarIndexerService; -import org.quiltmc.enigma.api.service.ObfuscationTestService; -import org.quiltmc.enigma.api.source.TokenType; -import org.quiltmc.enigma.api.translation.mapping.EntryResolver; -import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; -import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; import org.quiltmc.enigma.api.class_provider.ClassProvider; -import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; import org.quiltmc.enigma.api.service.DecompilerService; -import org.quiltmc.enigma.api.source.Decompiler; -import org.quiltmc.enigma.api.source.SourceSettings; -import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; -import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; -import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; -import org.quiltmc.enigma.impl.plugin.EnumConstantIndexingService; -import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker; -import org.quiltmc.enigma.util.I18n; -import org.tinylog.Logger; +import org.quiltmc.enigma.impl.EnigmaProjectImpl; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.stream.Collectors; import java.util.stream.Stream; -public class EnigmaProject { - private final Enigma enigma; - private final Path jarPath; - private final ClassProvider classProvider; - private final JarIndex jarIndex; - private final JarIndex libIndex; - private final JarIndex combinedIndex; - private final byte[] jarChecksum; - private final Map libraryMethodOverrideCache = new HashMap<>(); - - private EntryRemapper remapper; - private MappingsIndex mappingsIndex; - - public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { - Preconditions.checkArgument(jarChecksum.length == 20); - this.enigma = enigma; - this.jarPath = jarPath; - this.classProvider = classProvider; - this.jarIndex = jarIndex; - this.libIndex = libIndex; - this.combinedIndex = combinedIndex; - this.jarChecksum = jarChecksum; - - this.mappingsIndex = mappingsIndex; - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, proposedNames, new HashEntryTree<>(), this.enigma.getNameProposalServices()); +/** + * Represents an Enigma project which applies a set of mappings to a source jar. + * + * @implNote This interface is not intended for implementation by api users. An instance can be created using the + * {@link #of} factory method. + */ +public interface EnigmaProject { + // TODO should this be api? + static EnigmaProject of(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { + return new EnigmaProjectImpl(enigma, jarPath, classProvider, jarIndex, libIndex, combinedIndex, mappingsIndex, proposedNames, jarChecksum); } /** @@ -91,250 +37,49 @@ public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, J * @param mappings the new mappings * @param progress a progress listener for indexing */ - public void setMappings(@Nullable EntryTree mappings, ProgressListener progress) { - // keep bytecode-based proposed names, to avoid unnecessary recalculation - EntryTree jarProposedMappings = this.remapper != null ? this.remapper.getJarProposedMappings() : new HashEntryTree<>(); - - this.mappingsIndex = MappingsIndex.empty(); - - if (mappings != null) { - EntryTree mergedTree = EntryTreeUtil.merge(jarProposedMappings, mappings); - - this.mappingsIndex.indexMappings(mergedTree, progress); - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, mappings, this.enigma.getNameProposalServices()); - } else if (!jarProposedMappings.isEmpty()) { - this.mappingsIndex.indexMappings(jarProposedMappings, progress); - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, new HashEntryTree<>(), this.enigma.getNameProposalServices()); - } else { - this.remapper = EntryRemapper.empty(this.enigma, this.combinedIndex, this.enigma.getNameProposalServices()); - } + void setMappings(@Nullable EntryTree mappings, ProgressListener progress); - // update dynamically proposed names - this.remapper.insertDynamicallyProposedMappings(null, null, null); - } - - public Enigma getEnigma() { - return this.enigma; - } + Enigma getEnigma(); - public Path getJarPath() { - return this.jarPath; - } + Path getJarPath(); - public ClassProvider getClassProvider() { - return this.classProvider; - } + ClassProvider getClassProvider(); /** * Gets the index of the main jar of this project; the jar being mapped. */ - public JarIndex getJarIndex() { - return this.jarIndex; - } + JarIndex getJarIndex(); /** * Gets the index of the library jars of this project. */ - public JarIndex getLibIndex() { - return this.libIndex; - } + JarIndex getLibIndex(); /** * Gets the index of the main jar and library jars of this project. */ - public JarIndex getCombinedIndex() { - return this.combinedIndex; - } - - public MappingsIndex getMappingsIndex() { - return this.mappingsIndex; - } - - public byte[] getJarChecksum() { - return this.jarChecksum; - } - - public EntryRemapper getRemapper() { - return this.remapper; - } - - public Collection> dropMappings(ProgressListener progress) { - DeltaTrackingTree mappings = this.remapper.getMappings(); - - Collection> dropped = this.dropMappings(mappings, progress); - for (Entry entry : dropped) { - mappings.trackChange(entry); - } - - return dropped; - } - - private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { - MappingsChecker.Dropper dropper = new MappingsChecker.Dropper(); - - // drop mappings that don't match the jar - MappingsChecker checker = new MappingsChecker(this, this.jarIndex, mappings); - - checker.collectBrokenMappings(progress, dropper); - - Map, String> droppedBrokenMappings = dropper.getPendingDroppedMappings(); - for (Map.Entry, String> mapping : droppedBrokenMappings.entrySet()) { - Logger.warn("Couldn't find {} ({}) in jar. Mapping was dropped.", mapping.getKey(), mapping.getValue()); - } - - dropper.applyPendingDrops(mappings); - checker.collectEmptyMappings(progress, dropper); + JarIndex getCombinedIndex(); - Map, String> droppedEmptyMappings = dropper.getPendingDroppedMappings(); - for (Map.Entry, String> mapping : droppedEmptyMappings.entrySet()) { - Logger.warn("{} ({}) was empty. Mapping was dropped.", mapping.getKey(), mapping.getValue()); - } + MappingsIndex getMappingsIndex(); - dropper.applyPendingDrops(mappings); + // TODO should this be api? + byte[] getJarChecksum(); - return dropper.getDroppedMappings().keySet(); - } - - public boolean isNavigable(Entry obfEntry) { - if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { - return false; - } - - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); - } - - public boolean isInternallyRenamable(Entry obfEntry) { - if (obfEntry instanceof MethodEntry obfMethodEntry) { - // constructors are not renamable! - if (obfMethodEntry.isConstructor()) { - return false; - } + EntryRemapper getRemapper(); - // HACKHACK: Object methods are not obfuscated identifiers - String name = obfMethodEntry.getName(); - String sig = obfMethodEntry.getDesc().toString(); + Collection> dropMappings(ProgressListener progress); - // methods declared in object and record are not renamable - // note: compareTo ignores parent, we want that - if (this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Object")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0) - || this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Record")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0)) { - return false; - } + boolean isNavigable(Entry obfEntry); - ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(obfMethodEntry.getParent()); - if (parent != null && parent.isEnum() - && ((name.equals("values") && sig.equals("()[L" + parent.getFullName() + ";")) - || isEnumValueOfMethod(parent, obfMethodEntry))) { - return false; - } + boolean isRenamable(Entry obfEntry); - if (this.isLibraryMethodOverride(obfMethodEntry)) { - return false; - } - } else if (obfEntry instanceof LocalVariableEntry localEntry) { - if (!localEntry.isArgument()) { - return false; - } + boolean isRenamable(EntryReference, Entry> obfReference); - MethodEntry method = localEntry.getParent(); - ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(method.getParent()); + boolean isObfuscated(Entry entry); - // if this is the valueOf method of an enum class, the argument shouldn't be able to be renamed. - if (isEnumValueOfMethod(parent, method)) { - return false; - } - } else if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { - return false; - } + boolean isSynthetic(Entry entry); - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); - } - - private boolean isLibraryMethodOverride(MethodEntry methodEntry) { - final Boolean cached = this.libraryMethodOverrideCache.get(methodEntry); - if (cached != null) { - return cached; - } else { - if (this.combinedIndex.getIndex(EntryIndex.class).hasMethod(methodEntry)) { - final EntryResolver combinedResolver = this.combinedIndex.getEntryResolver(); - final Set equivalents = combinedResolver.resolveEquivalentMethods(methodEntry); - final Set roots = equivalents.stream() - .flatMap(equivalent -> combinedResolver.resolveEntry(equivalent, ResolutionStrategy.RESOLVE_ROOT).stream()) - .collect(Collectors.toSet()); - - final Set equivalentsAndRoots = Stream - .concat(equivalents.stream(), roots.stream()) - .collect(Collectors.toSet()); - - final EntryIndex jarEntryIndex = this.jarIndex.getIndex(EntryIndex.class); - final boolean anyNonJar = equivalentsAndRoots.stream().anyMatch(method -> !jarEntryIndex.hasMethod(method)); - - equivalentsAndRoots.forEach(method -> this.libraryMethodOverrideCache.put(method, anyNonJar)); - - return anyNonJar; - } else { - this.libraryMethodOverrideCache.put(methodEntry, false); - - return false; - } - } - } - - public boolean isRenamable(Entry obfEntry) { - if (this.isInternallyRenamable(obfEntry)) { - if (obfEntry instanceof FieldEntry fieldEntry) { - return !this.getEnumConstantIndexingService() - .map(service -> service.isEnumConstant(fieldEntry)) - .orElse(false); - } else { - return true; - } - } else { - return false; - } - } - - private Optional getEnumConstantIndexingService() { - return this.getEnigma() - .getService(JarIndexerService.TYPE, EnumConstantIndexingService.ID) - .map(service -> (EnumConstantIndexingService) service); - } - - private static boolean isEnumValueOfMethod(ClassDefEntry parent, MethodEntry method) { - return parent != null && parent.isEnum() && method.getName().equals("valueOf") && method.getDesc().toString().equals("(Ljava/lang/String;)L" + parent.getFullName() + ";"); - } - - public boolean isInternallyRenamable(EntryReference, Entry> obfReference) { - return obfReference.isNamed() && this.isInternallyRenamable(obfReference.getNameableEntry()); - } - - public boolean isRenamable(EntryReference, Entry> obfReference) { - return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); - } - - public boolean isObfuscated(Entry entry) { - List obfuscationTestServices = this.getEnigma().getServices().get(ObfuscationTestService.TYPE); - if (!obfuscationTestServices.isEmpty()) { - for (ObfuscationTestService service : obfuscationTestServices) { - if (service.testDeobfuscated(entry)) { - return false; - } - } - } - - EntryMapping mapping = this.remapper.getMapping(entry); - return mapping.tokenType() == TokenType.OBFUSCATED; - } - - public boolean isSynthetic(Entry entry) { - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(entry) && this.jarIndex.getIndex(EntryIndex.class).getEntryAccess(entry).isSynthetic(); - } - - public boolean isAnonymousOrLocal(ClassEntry classEntry) { - EnclosingMethodIndex enclosingMethodIndex = this.jarIndex.getIndex(EnclosingMethodIndex.class); - // Only local and anonymous classes may have the EnclosingMethod attribute - return enclosingMethodIndex.hasEnclosingMethod(classEntry); - } + boolean isAnonymousOrLocal(ClassEntry classEntry); /** * Verifies that the provided {@code parameter} has a valid index for its parent method. @@ -348,187 +93,33 @@ public boolean isAnonymousOrLocal(ClassEntry classEntry) { * @param parameter the parameter to validate * @return whether the index is valid */ - @SuppressWarnings("DataFlowIssue") - public boolean validateParameterIndex(LocalVariableEntry parameter) { - MethodEntry parent = parameter.getParent(); - EntryIndex index = this.jarIndex.getIndex(EntryIndex.class); + boolean validateParameterIndex(LocalVariableEntry parameter); - if (index.hasMethod(parent)) { - AtomicInteger maxLocals = new AtomicInteger(-1); - ClassEntry parentClass = parent.getParent(); + JarExport exportRemappedJar(ProgressListener progress); - // find max_locals for method, representing the number of parameters it receives (JVMS§4.7.3) - // note: parent class cannot be null, warning suppressed - ClassNode classNode = this.getClassProvider().get(parentClass.getFullName()); - if (classNode != null) { - classNode.methods.stream() - .filter(node -> node.name.equals(parent.getName()) && node.desc.equals(parent.getDesc().toString())) - .findFirst().ifPresent(node -> { - // occasionally it's possible to run into a method that has parameters, yet whose max locals is 0. java is stupid. we ignore those cases - if (!(node.parameters != null && node.parameters.size() > node.maxLocals)) { - maxLocals.set(node.maxLocals); - } - }); - } + interface JarExport { + void write(Path path, ProgressListener progress) throws IOException; - // if maxLocals is -1 it's not found for the method and should be ignored - return index.validateParameterIndex(parameter) && (maxLocals.get() == -1 || parameter.getIndex() <= maxLocals.get() - 1); - } + SourceExport decompile( + ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy + ); - return false; + Stream decompileStream( + ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy + ); } - public JarExport exportRemappedJar(ProgressListener progress) { - Collection classEntries = this.jarIndex.getIndex(EntryIndex.class).getClasses(); - ClassProvider fixingClassProvider = new ObfuscationFixClassProvider(this.classProvider, this.jarIndex); - Translator deobfuscator = this.remapper.getDeobfuscator(); - - AtomicInteger count = new AtomicInteger(); - progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); - - Map compiled = classEntries.parallelStream() - .map(entry -> { - ClassEntry translatedEntry = deobfuscator.translate(entry); - progress.step(count.getAndIncrement(), translatedEntry.toString()); - - ClassNode node = fixingClassProvider.get(entry.getFullName()); - if (node != null) { - ClassNode translatedNode = new ClassNode(); - node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, translatedNode)); - return translatedNode; - } - - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toMap(n -> n.name, Functions.identity())); - - return new JarExport(this.remapper, compiled); + interface SourceExport { + void write(Path path, ProgressListener progress) throws IOException; } - public static final class JarExport { - private final EntryRemapper mapper; - private final Map compiled; - - JarExport(EntryRemapper mapper, Map compiled) { - this.mapper = mapper; - this.compiled = compiled; - } - - public void write(Path path, ProgressListener progress) throws IOException { - progress.init(this.compiled.size(), I18n.translate("progress.jar.writing")); - - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) { - AtomicInteger count = new AtomicInteger(); - - for (ClassNode node : this.compiled.values()) { - progress.step(count.getAndIncrement(), node.name); - - String entryName = node.name.replace('.', '/') + ".class"; - - ClassWriter writer = new ClassWriter(0); - node.accept(writer); - - out.putNextEntry(new JarEntry(entryName)); - out.write(writer.toByteArray()); - out.closeEntry(); - } - } - } - - public SourceExport decompile(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { - List decompiled = this.decompileStream(progress, decompilerService, errorStrategy).toList(); - return new SourceExport(decompiled); - } - - public Stream decompileStream(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { - Collection classes = this.compiled.values().stream() - .filter(classNode -> classNode.name.indexOf('$') == -1) - .toList(); - - progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); - - //create a common instance outside the loop as mappings shouldn't be changing while this is happening - Decompiler decompiler = decompilerService.create(ClassProvider.fromMap(this.compiled), new SourceSettings(false, false)); - - AtomicInteger count = new AtomicInteger(); - - return classes.parallelStream() - .map(translatedNode -> { - progress.step(count.getAndIncrement(), translatedNode.name); - - String source = null; - try { - source = this.decompileClass(translatedNode, decompiler); - } catch (Exception e) { - switch (errorStrategy) { - case PROPAGATE: throw e; - case IGNORE: break; - case TRACE_AS_SOURCE: { - StringWriter writer = new StringWriter(); - e.printStackTrace(new PrintWriter(writer)); - source = writer.toString(); - break; - } - } - } - - if (source == null) { - return null; - } - - return new ClassSource(translatedNode.name, source); - }) - .filter(Objects::nonNull); - } - - private String decompileClass(ClassNode translatedNode, Decompiler decompiler) { - return decompiler.getSource(translatedNode.name, this.mapper).asString(); - } - } - - public static final class SourceExport { - public final Collection decompiled; - - SourceExport(Collection decompiled) { - this.decompiled = decompiled; - } - - public void write(Path path, ProgressListener progress) throws IOException { - progress.init(this.decompiled.size(), I18n.translate("progress.sources.writing")); - - int count = 0; - for (ClassSource source : this.decompiled) { - progress.step(count++, source.name); - - Path sourcePath = source.resolvePath(path); - source.writeTo(sourcePath); - } - } - } - - public static class ClassSource { - public final String name; - public final String source; - - ClassSource(String name, String source) { - this.name = name; - this.source = source; - } - - public void writeTo(Path path) throws IOException { - Files.createDirectories(path.getParent()); - try (BufferedWriter writer = Files.newBufferedWriter(path)) { - writer.write(this.source); - } - } + interface ClassSource { + void writeTo(Path path) throws IOException; - public Path resolvePath(Path root) { - return root.resolve(this.name.replace('.', '/') + ".java"); - } + Path resolvePath(Path root); } - public enum DecompileErrorStrategy { + enum DecompileErrorStrategy { PROPAGATE, TRACE_AS_SOURCE, IGNORE diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java index 7a0ef4c37..59eed99f7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java @@ -12,6 +12,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; +import org.quiltmc.enigma.impl.EnigmaProjectImpl; import javax.swing.tree.DefaultMutableTreeNode; import java.util.ArrayList; @@ -38,6 +39,10 @@ public ParentedEntry getEntry() { } public void load(EnigmaProject project, StructureTreeOptions options) { + this.loadImpl((EnigmaProjectImpl) project, options); + } + + private void loadImpl(EnigmaProjectImpl project, StructureTreeOptions options) { Stream> children = project.getJarIndex().getChildrenByClass().get(this.parentEntry).stream(); children = switch (options.obfuscationVisibility()) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java index 086884876..63eed2a13 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java @@ -10,6 +10,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableDefEntry; +import org.quiltmc.enigma.impl.EnigmaProjectImpl; import org.quiltmc.enigma.impl.translation.LocalNameGenerator; import java.util.Collection; @@ -46,12 +47,12 @@ public DecompiledClassSource remapSource(EnigmaProject project, Translator trans SourceRemapper remapper = new SourceRemapper(this.obfuscatedIndex.getSource(), this.obfuscatedIndex.referenceTokens()); TokenStore tokenStore = TokenStore.create(this.obfuscatedIndex); - SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> this.remapToken(tokenStore, project, token, movedToken, translator)); + SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> this.remapToken(tokenStore, ((EnigmaProjectImpl) project), token, movedToken, translator)); SourceIndex remappedIndex = this.obfuscatedIndex.remapTo(remapResult); return new DecompiledClassSource(this.classEntry, this.obfuscatedIndex, remappedIndex, tokenStore); } - private String remapToken(TokenStore target, EnigmaProject project, Token token, Token movedToken, Translator translator) { + private String remapToken(TokenStore target, EnigmaProjectImpl project, Token token, Token movedToken, Translator translator) { EntryReference, Entry> reference = this.obfuscatedIndex.getReference(token); Entry entry = this.obfuscatedIndex.remapToNameable ? reference.getNameableEntry() : reference.entry; diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java new file mode 100644 index 000000000..ba0a65d67 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java @@ -0,0 +1,526 @@ +package org.quiltmc.enigma.impl; + +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.analysis.index.jar.EnclosingMethodIndex; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; +import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; +import org.quiltmc.enigma.api.service.DecompilerService; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.service.ObfuscationTestService; +import org.quiltmc.enigma.api.source.Decompiler; +import org.quiltmc.enigma.api.source.SourceSettings; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.Translator; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; +import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; +import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; +import org.quiltmc.enigma.impl.plugin.EnumConstantIndexingService; +import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker; +import org.quiltmc.enigma.util.I18n; +import org.tinylog.Logger; + +import javax.annotation.Nullable; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EnigmaProjectImpl implements EnigmaProject { + private final Enigma enigma; + private final Path jarPath; + private final ClassProvider classProvider; + private final JarIndex jarIndex; + private final JarIndex libIndex; + private final JarIndex combinedIndex; + private final byte[] jarChecksum; + private final Map libraryMethodOverrideCache = new HashMap<>(); + + private EntryRemapper remapper; + private MappingsIndex mappingsIndex; + + public EnigmaProjectImpl(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { + Preconditions.checkArgument(jarChecksum.length == 20); + this.enigma = enigma; + this.jarPath = jarPath; + this.classProvider = classProvider; + this.jarIndex = jarIndex; + this.libIndex = libIndex; + this.combinedIndex = combinedIndex; + this.jarChecksum = jarChecksum; + + this.mappingsIndex = mappingsIndex; + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, proposedNames, new HashEntryTree<>(), this.enigma.getNameProposalServices()); + } + + @Override + public void setMappings(@Nullable EntryTree mappings, ProgressListener progress) { + // keep bytecode-based proposed names, to avoid unnecessary recalculation + EntryTree jarProposedMappings = this.remapper != null ? this.remapper.getJarProposedMappings() : new HashEntryTree<>(); + + this.mappingsIndex = MappingsIndex.empty(); + + if (mappings != null) { + EntryTree mergedTree = EntryTreeUtil.merge(jarProposedMappings, mappings); + + this.mappingsIndex.indexMappings(mergedTree, progress); + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, mappings, this.enigma.getNameProposalServices()); + } else if (!jarProposedMappings.isEmpty()) { + this.mappingsIndex.indexMappings(jarProposedMappings, progress); + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, new HashEntryTree<>(), this.enigma.getNameProposalServices()); + } else { + this.remapper = EntryRemapper.empty(this.enigma, this.combinedIndex, this.enigma.getNameProposalServices()); + } + + // update dynamically proposed names + this.remapper.insertDynamicallyProposedMappings(null, null, null); + } + + @Override + public Enigma getEnigma() { + return this.enigma; + } + + @Override + public Path getJarPath() { + return this.jarPath; + } + + @Override + public ClassProvider getClassProvider() { + return this.classProvider; + } + + @Override + public JarIndex getJarIndex() { + return this.jarIndex; + } + + @Override + public JarIndex getLibIndex() { + return this.libIndex; + } + + @Override + public JarIndex getCombinedIndex() { + return this.combinedIndex; + } + + @Override + public MappingsIndex getMappingsIndex() { + return this.mappingsIndex; + } + + @Override + public byte[] getJarChecksum() { + return Arrays.copyOf(this.jarChecksum, this.jarChecksum.length); + } + + @Override + public EntryRemapper getRemapper() { + return this.remapper; + } + + @Override + public Collection> dropMappings(ProgressListener progress) { + DeltaTrackingTree mappings = this.remapper.getMappings(); + + Collection> dropped = this.dropMappings(mappings, progress); + for (Entry entry : dropped) { + mappings.trackChange(entry); + } + + return dropped; + } + + private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { + MappingsChecker.Dropper dropper = new MappingsChecker.Dropper(); + + // drop mappings that don't match the jar + MappingsChecker checker = new MappingsChecker(this, this.jarIndex, mappings); + + checker.collectBrokenMappings(progress, dropper); + + Map, String> droppedBrokenMappings = dropper.getPendingDroppedMappings(); + for (Map.Entry, String> mapping : droppedBrokenMappings.entrySet()) { + Logger.warn("Couldn't find {} ({}) in jar. Mapping was dropped.", mapping.getKey(), mapping.getValue()); + } + + dropper.applyPendingDrops(mappings); + checker.collectEmptyMappings(progress, dropper); + + Map, String> droppedEmptyMappings = dropper.getPendingDroppedMappings(); + for (Map.Entry, String> mapping : droppedEmptyMappings.entrySet()) { + Logger.warn("{} ({}) was empty. Mapping was dropped.", mapping.getKey(), mapping.getValue()); + } + + dropper.applyPendingDrops(mappings); + + return dropper.getDroppedMappings().keySet(); + } + + @Override + public boolean isNavigable(Entry obfEntry) { + if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { + return false; + } + + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); + } + + public boolean isInternallyRenamable(Entry obfEntry) { + if (obfEntry instanceof MethodEntry obfMethodEntry) { + // constructors are not renamable! + if (obfMethodEntry.isConstructor()) { + return false; + } + + // HACKHACK: Object methods are not obfuscated identifiers + String name = obfMethodEntry.getName(); + String sig = obfMethodEntry.getDesc().toString(); + + // methods declared in object and record are not renamable + // note: compareTo ignores parent, we want that + if (this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Object")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0) + || this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Record")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0)) { + return false; + } + + ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(obfMethodEntry.getParent()); + if (parent != null && parent.isEnum() + && ((name.equals("values") && sig.equals("()[L" + parent.getFullName() + ";")) + || isEnumValueOfMethod(parent, obfMethodEntry))) { + return false; + } + + if (this.isLibraryMethodOverride(obfMethodEntry)) { + return false; + } + } else if (obfEntry instanceof LocalVariableEntry localEntry) { + if (!localEntry.isArgument()) { + return false; + } + + MethodEntry method = localEntry.getParent(); + ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(method.getParent()); + + // if this is the valueOf method of an enum class, the argument shouldn't be able to be renamed. + if (isEnumValueOfMethod(parent, method)) { + return false; + } + } else if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { + return false; + } + + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); + } + + private boolean isLibraryMethodOverride(MethodEntry methodEntry) { + final Boolean cached = this.libraryMethodOverrideCache.get(methodEntry); + if (cached != null) { + return cached; + } else { + if (this.combinedIndex.getIndex(EntryIndex.class).hasMethod(methodEntry)) { + final EntryResolver combinedResolver = this.combinedIndex.getEntryResolver(); + final Set equivalents = combinedResolver.resolveEquivalentMethods(methodEntry); + final Set roots = equivalents.stream() + .flatMap(equivalent -> combinedResolver.resolveEntry(equivalent, ResolutionStrategy.RESOLVE_ROOT).stream()) + .collect(Collectors.toSet()); + + final Set equivalentsAndRoots = Stream + .concat(equivalents.stream(), roots.stream()) + .collect(Collectors.toSet()); + + final EntryIndex jarEntryIndex = this.jarIndex.getIndex(EntryIndex.class); + final boolean anyNonJar = equivalentsAndRoots.stream().anyMatch(method -> !jarEntryIndex.hasMethod(method)); + + equivalentsAndRoots.forEach(method -> this.libraryMethodOverrideCache.put(method, anyNonJar)); + + return anyNonJar; + } else { + this.libraryMethodOverrideCache.put(methodEntry, false); + + return false; + } + } + } + + @Override + public boolean isRenamable(Entry obfEntry) { + if (this.isInternallyRenamable(obfEntry)) { + if (obfEntry instanceof FieldEntry fieldEntry) { + return !this.getEnumConstantIndexingService() + .map(service -> service.isEnumConstant(fieldEntry)) + .orElse(false); + } else { + return true; + } + } else { + return false; + } + } + + private Optional getEnumConstantIndexingService() { + return this.getEnigma() + .getService(JarIndexerService.TYPE, EnumConstantIndexingService.ID) + .map(service -> (EnumConstantIndexingService) service); + } + + private static boolean isEnumValueOfMethod(ClassDefEntry parent, MethodEntry method) { + return parent != null && parent.isEnum() && method.getName().equals("valueOf") && method.getDesc().toString().equals("(Ljava/lang/String;)L" + parent.getFullName() + ";"); + } + + public boolean isInternallyRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && this.isInternallyRenamable(obfReference.getNameableEntry()); + } + + @Override + public boolean isRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); + } + + @Override + public boolean isObfuscated(Entry entry) { + List obfuscationTestServices = this.getEnigma().getServices().get(ObfuscationTestService.TYPE); + if (!obfuscationTestServices.isEmpty()) { + for (ObfuscationTestService service : obfuscationTestServices) { + if (service.testDeobfuscated(entry)) { + return false; + } + } + } + + EntryMapping mapping = this.remapper.getMapping(entry); + return mapping.tokenType() == TokenType.OBFUSCATED; + } + + @Override + public boolean isSynthetic(Entry entry) { + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(entry) && this.jarIndex.getIndex(EntryIndex.class).getEntryAccess(entry).isSynthetic(); + } + + @Override + public boolean isAnonymousOrLocal(ClassEntry classEntry) { + EnclosingMethodIndex enclosingMethodIndex = this.jarIndex.getIndex(EnclosingMethodIndex.class); + // Only local and anonymous classes may have the EnclosingMethod attribute + return enclosingMethodIndex.hasEnclosingMethod(classEntry); + } + + @Override + @SuppressWarnings("DataFlowIssue") + public boolean validateParameterIndex(LocalVariableEntry parameter) { + MethodEntry parent = parameter.getParent(); + EntryIndex index = this.jarIndex.getIndex(EntryIndex.class); + + if (index.hasMethod(parent)) { + AtomicInteger maxLocals = new AtomicInteger(-1); + ClassEntry parentClass = parent.getParent(); + + // find max_locals for method, representing the number of parameters it receives (JVMS§4.7.3) + // note: parent class cannot be null, warning suppressed + ClassNode classNode = this.getClassProvider().get(parentClass.getFullName()); + if (classNode != null) { + classNode.methods.stream() + .filter(node -> node.name.equals(parent.getName()) && node.desc.equals(parent.getDesc().toString())) + .findFirst().ifPresent(node -> { + // occasionally it's possible to run into a method that has parameters, yet whose max locals is 0. java is stupid. we ignore those cases + if (!(node.parameters != null && node.parameters.size() > node.maxLocals)) { + maxLocals.set(node.maxLocals); + } + }); + } + + // if maxLocals is -1 it's not found for the method and should be ignored + return index.validateParameterIndex(parameter) && (maxLocals.get() == -1 || parameter.getIndex() <= maxLocals.get() - 1); + } + + return false; + } + + @Override + public JarExportImpl exportRemappedJar(ProgressListener progress) { + Collection classEntries = this.jarIndex.getIndex(EntryIndex.class).getClasses(); + ClassProvider fixingClassProvider = new ObfuscationFixClassProvider(this.classProvider, this.jarIndex); + Translator deobfuscator = this.remapper.getDeobfuscator(); + + AtomicInteger count = new AtomicInteger(); + progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); + + Map compiled = classEntries.parallelStream() + .map(entry -> { + ClassEntry translatedEntry = deobfuscator.translate(entry); + progress.step(count.getAndIncrement(), translatedEntry.toString()); + + ClassNode node = fixingClassProvider.get(entry.getFullName()); + if (node != null) { + ClassNode translatedNode = new ClassNode(); + node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, translatedNode)); + return translatedNode; + } + + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(n -> n.name, Functions.identity())); + + return new JarExportImpl(this.remapper, compiled); + } + + public static final class JarExportImpl implements JarExport { + private final EntryRemapper mapper; + private final Map compiled; + + JarExportImpl(EntryRemapper mapper, Map compiled) { + this.mapper = mapper; + this.compiled = compiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(this.compiled.size(), I18n.translate("progress.jar.writing")); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) { + AtomicInteger count = new AtomicInteger(); + + for (ClassNode node : this.compiled.values()) { + progress.step(count.getAndIncrement(), node.name); + + String entryName = node.name.replace('.', '/') + ".class"; + + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + + out.putNextEntry(new JarEntry(entryName)); + out.write(writer.toByteArray()); + out.closeEntry(); + } + } + } + + public SourceExport decompile(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { + List decompiled = this.decompileStream(progress, decompilerService, errorStrategy).toList(); + return new SourceExportImpl(decompiled); + } + + public Stream decompileStream(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { + Collection classes = this.compiled.values().stream() + .filter(classNode -> classNode.name.indexOf('$') == -1) + .toList(); + + progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); + + //create a common instance outside the loop as mappings shouldn't be changing while this is happening + Decompiler decompiler = decompilerService.create(ClassProvider.fromMap(this.compiled), new SourceSettings(false, false)); + + AtomicInteger count = new AtomicInteger(); + + return classes.parallelStream() + .map(translatedNode -> { + progress.step(count.getAndIncrement(), translatedNode.name); + + String source = null; + try { + source = this.decompileClass(translatedNode, decompiler); + } catch (Exception e) { + switch (errorStrategy) { + case PROPAGATE: throw e; + case IGNORE: break; + case TRACE_AS_SOURCE: { + StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + source = writer.toString(); + break; + } + } + } + + if (source == null) { + return null; + } + + return new ClassSourceImpl(translatedNode.name, source); + }) + .filter(Objects::nonNull); + } + + private String decompileClass(ClassNode translatedNode, Decompiler decompiler) { + return decompiler.getSource(translatedNode.name, this.mapper).asString(); + } + } + + public static final class SourceExportImpl implements SourceExport { + public final Collection decompiled; + + SourceExportImpl(Collection decompiled) { + this.decompiled = decompiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(this.decompiled.size(), I18n.translate("progress.sources.writing")); + + int count = 0; + for (ClassSourceImpl source : this.decompiled) { + progress.step(count++, source.name); + + Path sourcePath = source.resolvePath(path); + source.writeTo(sourcePath); + } + } + } + + public static class ClassSourceImpl implements ClassSource { + public final String name; + public final String source; + + ClassSourceImpl(String name, String source) { + this.name = name; + this.source = source; + } + + public void writeTo(Path path) throws IOException { + Files.createDirectories(path.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(this.source); + } + } + + public Path resolvePath(Path root) { + return root.resolve(this.name.replace('.', '/') + ".java"); + } + } +} From b94419cca2f3ea4387518d6e56adcd0b2e146e08 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 07:17:32 -0800 Subject: [PATCH 16/17] move new classes to jspecify annotations --- .../main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java | 2 +- .../quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java | 2 +- .../quiltmc/enigma/impl/plugin/EnumConstantProposalService.java | 2 +- .../quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java index ba0a65d67..60d86e201 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java @@ -2,6 +2,7 @@ import com.google.common.base.Functions; import com.google.common.base.Preconditions; +import org.jspecify.annotations.Nullable; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; import org.quiltmc.enigma.api.Enigma; @@ -41,7 +42,6 @@ import org.quiltmc.enigma.util.I18n; import org.tinylog.Logger; -import javax.annotation.Nullable; import java.io.BufferedWriter; import java.io.IOException; import java.io.PrintWriter; diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java index d92445943..671390a4e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java @@ -1,12 +1,12 @@ package org.quiltmc.enigma.impl.plugin; +import org.jspecify.annotations.Nullable; import org.objectweb.asm.tree.ClassNode; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import javax.annotation.Nullable; import java.util.Set; public class EnumConstantIndexingService implements JarIndexerService { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java index 67eb68acc..a878c3ec0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.impl.plugin; +import org.jspecify.annotations.Nullable; import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; @@ -9,7 +10,6 @@ import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java index fa706c276..af5014647 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.impl.plugin; import org.jetbrains.java.decompiler.util.Pair; +import org.jspecify.annotations.Nullable; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; @@ -20,7 +21,6 @@ import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; From 9bf327078de3e1209747a798a3d658993ff39d02 Mon Sep 17 00:00:00 2001 From: supersaiyansubtlety Date: Tue, 18 Nov 2025 07:20:51 -0800 Subject: [PATCH 17/17] checkstyle --- .../org/quiltmc/enigma/records/TestRecordComponentProposal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java index 4dc983254..ece7575ee 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -177,7 +177,7 @@ void testIllegalGetterNameExclusion() { final MethodEntry stringGetter = TestEntryFactory .newMethod(stringComponentOverrideGetterRecord, "a", stringGetterDesc); final MethodEntry toString = TestEntryFactory - .newMethod(stringComponentOverrideGetterRecord, "toString", stringGetterDesc); + .newMethod(stringComponentOverrideGetterRecord, "toString", stringGetterDesc); Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringField).tokenType()); Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(stringGetter).tokenType());