diff --git a/build.gradle b/build.gradle index 43ed89fea..7fc5eaed2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,9 +6,6 @@ subprojects { apply plugin: 'java' apply plugin: 'maven-publish' - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - repositories { mavenLocal() mavenCentral() @@ -33,6 +30,7 @@ subprojects { testImplementation libs.junit testRuntimeOnly libs.junit.engine + testRuntimeOnly libs.junit.launcher testImplementation libs.hamcrest } @@ -44,6 +42,8 @@ subprojects { java { withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } tasks.withType(JavaCompile).configureEach { diff --git a/enigma-cli/build.gradle b/enigma-cli/build.gradle index c45ce1dc6..152d22ca5 100644 --- a/enigma-cli/build.gradle +++ b/enigma-cli/build.gradle @@ -9,9 +9,10 @@ dependencies { testImplementation(testFixtures(project(':enigma'))) } -mainClassName = 'org.quiltmc.enigma.command.Main' - -jar.manifest.attributes 'Main-Class': mainClassName +application { + mainClass = 'org.quiltmc.enigma.command.Main' + jar.manifest.attributes 'Main-Class': mainClass +} publishing { publications { diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Argument.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Argument.java index 11ab1c9a0..f91274270 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Argument.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Argument.java @@ -17,18 +17,6 @@ public enum Argument { """ A path to the right file or folder to read mappings from, used in commands which take two mapping inputs.""" ), - OUTPUT_MAPPING_FORMAT("", - """ - The mapping format to use when writing output mappings. Allowed values are (case-insensitive): - - TINY_V2:from_namespace:to_namespace (ex: tiny_v2:intermediary:named) - - ENIGMA_FILE - - ENIGMA_DIRECTORY - - ENIGMA_ZIP - - SRG_FILE - - RECAF - - Proguard is not a valid output format, as writing is unsupported.""" - ), MAPPING_OUTPUT("", """ A path to the file or folder to write mappings to. Will be created if missing.""" @@ -60,6 +48,14 @@ The decompiler to use when producing output. Allowed values are (case-insensitiv ENIGMA_PROFILE("", """ A path to an Enigma profile JSON file, used to apply things like plugins.""" + ), + OBFUSCATED_NAMESPACE("", + """ + The namespace to use for obfuscated names when writing mappings. Only used in certain mapping formats.""" + ), + DEOBFUSCATED_NAMESPACE("", + """ + The namespace to use for deobfuscated names when writing mappings. Only used in certain mapping formats.""" ); private final String displayForm; diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/CheckMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/CheckMappingsCommand.java index 5d889296c..652d55463 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/CheckMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/CheckMappingsCommand.java @@ -13,7 +13,8 @@ public class CheckMappingsCommand extends Command { public CheckMappingsCommand() { super(Argument.INPUT_JAR.required(), - Argument.INPUT_MAPPINGS.required()); + Argument.INPUT_MAPPINGS.required() + ); } @Override diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java index d25083cc4..2ab68ed85 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java @@ -12,7 +12,7 @@ import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.MappingDelta; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.tinylog.Logger; @@ -123,7 +123,7 @@ public static Enigma createEnigma() { * @return the argument, as a string */ protected String getArg(String[] args, int index) { - if (index < this.allArguments.size()) { + if (index < this.allArguments.size() && index >= 0) { return getArg(args, index, this.allArguments.get(index)); } else { throw new RuntimeException("arg index is outside of range of possible arguments! (index: " + index + ", allowed arg count: " + this.allArguments.size() + ")"); @@ -153,7 +153,7 @@ public static EnigmaProject openProject(Path fileJarIn, Path fileMappings, Enigm if (fileMappings != null) { Logger.info("Reading mappings..."); - EntryTree mappings = readMappings(fileMappings, progress); + EntryTree mappings = readMappings(enigma, fileMappings, progress); project.setMappings(mappings, new ConsoleProgressListener()); } @@ -161,9 +161,9 @@ public static EnigmaProject openProject(Path fileJarIn, Path fileMappings, Enigm return project; } - protected static EntryTree readMappings(Path path, ProgressListener progress) throws MappingParseException, IOException { - MappingFormat format = MappingFormat.parseFromFile(path); - return format.read(path, progress); + protected static EntryTree readMappings(Enigma enigma, Path path, ProgressListener progress) throws MappingParseException, IOException { + MappingsReader reader = CommandsUtil.getReader(enigma, path); + return reader.read(path, progress); } protected static File getWritableFile(String path) { diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/CommandsUtil.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/CommandsUtil.java new file mode 100644 index 000000000..b7c503940 --- /dev/null +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/CommandsUtil.java @@ -0,0 +1,39 @@ +package org.quiltmc.enigma.command; + +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; + +import java.nio.file.Path; + +public class CommandsUtil { + public static ReadWriteService getReadWriteService(Enigma enigma, Path file) { + var service = enigma.getReadWriteService(file); + if (service.isEmpty()) { + throw new UnsupportedOperationException("No reader/writer found for file \"" + file + "\""); + } + + return service.get(); + } + + public static MappingsReader getReader(Enigma enigma, Path file) { + ReadWriteService service = getReadWriteService(enigma, file); + + if (!service.supportsReading()) { + throw new UnsupportedOperationException("Read/write service for file \"" + file + "\" does not support reading!"); + } + + return service; + } + + public static MappingsWriter getWriter(Enigma enigma, Path file) { + ReadWriteService service = getReadWriteService(enigma, file); + + if (!service.supportsWriting()) { + throw new UnsupportedOperationException("Read/write service for file \"" + file + "\" does not support writing!"); + } + + return service; + } +} diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/ComposeMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/ComposeMappingsCommand.java index 4d1a8cf45..263b485cb 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/ComposeMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/ComposeMappingsCommand.java @@ -1,8 +1,9 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.util.MappingOperations; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; @@ -11,6 +12,7 @@ import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.util.Utils; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Path; @@ -18,20 +20,21 @@ public class ComposeMappingsCommand extends Command { public ComposeMappingsCommand() { super(Argument.LEFT_MAPPINGS.required(), Argument.RIGHT_MAPPINGS.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), Argument.MAPPING_OUTPUT.required(), - Argument.KEEP_MODE.required()); + Argument.KEEP_MODE.required() + ); } @Override public void run(String... args) throws IOException, MappingParseException { Path left = getReadablePath(this.getArg(args, 0)); Path right = getReadablePath(this.getArg(args, 1)); - String resultFormat = this.getArg(args, 2); - Path result = getWritablePath(this.getArg(args, 3)); - String keepMode = this.getArg(args, 4); + Path result = getWritablePath(this.getArg(args, 2)); + String keepMode = this.getArg(args, 3); + String obfuscatedNamespace = this.getArg(args, 4); + String deobfuscatedNamespace = this.getArg(args, 5); - run(left, right, resultFormat, result, keepMode); + run(left, right, result, keepMode, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -44,17 +47,18 @@ public String getDescription() { return "Merges the two mapping trees (left and right) into a common (middle) name set, handling conflicts according to the given \"keep mode\"."; } - public static void run(Path leftFile, Path rightFile, String resultFormat, Path resultFile, String keepMode) throws IOException, MappingParseException { - MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + public static void run(Path leftFile, Path rightFile, Path resultFile, String keepMode, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws IOException, MappingParseException { + MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, obfuscatedNamespace, deobfuscatedNamespace); + Enigma enigma = createEnigma(); - MappingFormat leftFormat = MappingFormat.parseFromFile(leftFile); - EntryTree left = leftFormat.read(leftFile); - MappingFormat rightFormat = MappingFormat.parseFromFile(rightFile); - EntryTree right = rightFormat.read(rightFile); + MappingsReader leftReader = CommandsUtil.getReader(enigma, leftFile); + EntryTree left = leftReader.read(leftFile); + MappingsReader rightReader = CommandsUtil.getReader(enigma, rightFile); + EntryTree right = rightReader.read(rightFile); EntryTree result = MappingOperations.compose(left, right, keepMode.equals("left") || keepMode.equals("both"), keepMode.equals("right") || keepMode.equals("both")); + MappingsWriter writer = CommandsUtil.getWriter(enigma, resultFile); Utils.delete(resultFile); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); writer.write(result, resultFile, ProgressListener.createEmpty(), saveParameters); } } diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/ConvertMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/ConvertMappingsCommand.java index b3d6dc7c9..2b2a7bc66 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/ConvertMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/ConvertMappingsCommand.java @@ -1,32 +1,37 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.util.Utils; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Path; public class ConvertMappingsCommand extends Command { public ConvertMappingsCommand() { super(Argument.INPUT_MAPPINGS.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), - Argument.MAPPING_OUTPUT.required()); + Argument.MAPPING_OUTPUT.required(), + Argument.OBFUSCATED_NAMESPACE.required(), + Argument.DEOBFUSCATED_NAMESPACE.required() + ); } @Override public void run(String... args) throws IOException, MappingParseException { Path source = getReadablePath(this.getArg(args, 0)); - String resultFormat = this.getArg(args, 1); - Path result = getWritablePath(this.getArg(args, 2)); + Path result = getWritablePath(this.getArg(args, 1)); + String obfuscatedNamespace = this.getArg(args, 2); + String deobfuscatedNamespace = this.getArg(args, 3); - run(source, resultFormat, result); + run(source, result, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -39,14 +44,15 @@ public String getDescription() { return "Converts the provided mappings to a different format."; } - public static void run(Path source, String resultFormat, Path output) throws MappingParseException, IOException { - MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + public static void run(Path source, Path output, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws MappingParseException, IOException { + MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, obfuscatedNamespace, deobfuscatedNamespace); + Enigma enigma = createEnigma(); - MappingFormat format = MappingFormat.parseFromFile(source); - EntryTree mappings = format.read(source); + MappingsReader reader = CommandsUtil.getReader(enigma, source); + EntryTree mappings = reader.read(source); Utils.delete(output); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); + MappingsWriter writer = CommandsUtil.getWriter(enigma, output); writer.write(mappings, output, ProgressListener.createEmpty(), saveParameters); } } diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DecompileCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DecompileCommand.java index bfafc2a36..8e3c7446a 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DecompileCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DecompileCommand.java @@ -3,7 +3,7 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.EnigmaProject.DecompileErrorStrategy; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Decompilers; import org.tinylog.Logger; diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DeobfuscateCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DeobfuscateCommand.java index 24b377443..5944f409a 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DeobfuscateCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DeobfuscateCommand.java @@ -9,7 +9,8 @@ public class DeobfuscateCommand extends Command { public DeobfuscateCommand() { super(Argument.INPUT_JAR.required(), Argument.OUTPUT_JAR.required(), - Argument.INPUT_MAPPINGS.optional()); + Argument.INPUT_MAPPINGS.optional() + ); } @Override diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DropInvalidMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DropInvalidMappingsCommand.java index f398326fc..496e3049c 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/DropInvalidMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/DropInvalidMappingsCommand.java @@ -2,8 +2,8 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; import org.tinylog.Logger; import java.io.IOException; @@ -46,7 +46,7 @@ public static void run(Path jarIn, Path mappingsIn, Path mappingsOut) throws Exc return; } - MappingFormat format = MappingFormat.parseFromFile(mappingsIn); + MappingsWriter writer = CommandsUtil.getWriter(createEnigma(), mappingsIn); EnigmaProject project = openProject(jarIn, mappingsIn); Logger.info("Dropping invalid mappings..."); @@ -75,6 +75,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } MappingSaveParameters saveParameters = project.getEnigma().getProfile().getMappingSaveParameters(); - format.write(project.getRemapper().getMappings(), mappingsOut, ProgressListener.createEmpty(), saveParameters); + writer.write(project.getRemapper().getMappings(), mappingsOut, ProgressListener.createEmpty(), saveParameters); } } diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java index 4905313e1..947d5ebea 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; @@ -15,6 +16,7 @@ import org.quiltmc.enigma.util.Utils; import org.tinylog.Logger; +import javax.annotation.Nullable; import java.nio.file.Path; import java.util.List; @@ -23,8 +25,10 @@ protected FillClassMappingsCommand() { super(Argument.INPUT_JAR.required(), Argument.INPUT_MAPPINGS.required(), Argument.MAPPING_OUTPUT.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), - Argument.FILL_ALL.optional()); + Argument.FILL_ALL.optional(), + Argument.OBFUSCATED_NAMESPACE.optional(), + Argument.DEOBFUSCATED_NAMESPACE.optional() + ); } @Override @@ -32,10 +36,11 @@ public void run(String... args) throws Exception { Path inJar = getReadablePath(this.getArg(args, 0)); Path source = getReadablePath(this.getArg(args, 1)); Path result = getWritablePath(this.getArg(args, 2)); - String resultFormat = this.getArg(args, 3); - boolean fillAll = Boolean.parseBoolean(this.getArg(args, 4)); + boolean fillAll = Boolean.parseBoolean(this.getArg(args, 3)); + String obfuscatedNamespace = this.getArg(args, 4); + String deobfuscatedNamespace = this.getArg(args, 5); - run(inJar, source, result, resultFormat, fillAll); + run(inJar, source, result, fillAll, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -48,19 +53,20 @@ public String getDescription() { return "Adds empty mappings for classes missing in the input file, but whose parent or ancestors, do have names"; } - public static void run(Path jar, Path source, Path result, String resultFormat, boolean fillAll) throws Exception { + public static void run(Path jar, Path source, Path result, boolean fillAll, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws Exception { boolean debug = shouldDebug(new FillClassMappingsCommand().getName()); JarIndex jarIndex = loadJar(jar); + Enigma enigma = createEnigma(); Logger.info("Reading mappings..."); - MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); - EntryTree sourceMappings = readMappings(source, ProgressListener.createEmpty()); + MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, obfuscatedNamespace, deobfuscatedNamespace); + EntryTree sourceMappings = readMappings(enigma, source, ProgressListener.createEmpty()); EntryTree resultMappings = exec(jarIndex, sourceMappings, fillAll, debug); Logger.info("Writing mappings..."); + MappingsWriter writer = CommandsUtil.getWriter(enigma, result); Utils.delete(result); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); writer.write(resultMappings, result, ProgressListener.createEmpty(), saveParameters); if (debug) { diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/InsertProposedMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/InsertProposedMappingsCommand.java index ccccb82f2..fdd7a279e 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/InsertProposedMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/InsertProposedMappingsCommand.java @@ -31,8 +31,10 @@ public InsertProposedMappingsCommand() { super(Argument.INPUT_JAR.required(), Argument.INPUT_MAPPINGS.required(), Argument.MAPPING_OUTPUT.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), - Argument.ENIGMA_PROFILE.optional()); + Argument.ENIGMA_PROFILE.optional(), + Argument.OBFUSCATED_NAMESPACE.optional(), + Argument.DEOBFUSCATED_NAMESPACE.optional() + ); } @Override @@ -40,10 +42,11 @@ public void run(String... args) throws Exception { Path inJar = getReadablePath(this.getArg(args, 0)); Path source = getReadablePath(this.getArg(args, 1)); Path output = getWritablePath(this.getArg(args, 2)); - String resultFormat = this.getArg(args, 3); - Path profilePath = getReadablePath(this.getArg(args, 4)); + Path profilePath = getReadablePath(this.getArg(args, 3)); + String obfuscatedNamespace = this.getArg(args, 4); + String deobfuscatedNamespace = this.getArg(args, 5); - run(inJar, source, output, resultFormat, profilePath, null); + run(inJar, source, output, profilePath, null, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -56,14 +59,14 @@ public String getDescription() { return "Adds all mappings proposed by the plugins on the classpath and declared in the profile into the given mappings."; } - public static void run(Path inJar, Path source, Path output, String resultFormat, @Nullable Path profilePath, @Nullable Iterable plugins) throws Exception { + public static void run(Path inJar, Path source, Path output, @Nullable Path profilePath, @Nullable Iterable plugins, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws Exception { EnigmaProfile profile = EnigmaProfile.read(profilePath); Enigma enigma = createEnigma(profile, plugins); - run(inJar, source, output, resultFormat, enigma); + run(inJar, source, output, enigma, obfuscatedNamespace, deobfuscatedNamespace); } - public static void run(Path inJar, Path source, Path output, String resultFormat, Enigma enigma) throws Exception { + public static void run(Path inJar, Path source, Path output, Enigma enigma, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws Exception { boolean debug = shouldDebug(new InsertProposedMappingsCommand().getName()); int nameProposalServices = enigma.getServices().get(NameProposalService.TYPE).size(); if (nameProposalServices == 0) { @@ -76,8 +79,9 @@ public static void run(Path inJar, Path source, Path output, String resultFormat printStats(project); Utils.delete(output); - MappingSaveParameters saveParameters = new MappingSaveParameters(enigma.getProfile().getMappingSaveParameters().fileNameFormat(), true); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); + MappingSaveParameters saveParameters = new MappingSaveParameters(enigma.getProfile().getMappingSaveParameters().fileNameFormat(), true, obfuscatedNamespace, deobfuscatedNamespace); + + MappingsWriter writer = CommandsUtil.getWriter(enigma, output); writer.write(mappings, output, ProgressListener.createEmpty(), saveParameters); if (debug) { diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/InvertMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/InvertMappingsCommand.java index cc5173598..ed5fe916c 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/InvertMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/InvertMappingsCommand.java @@ -1,32 +1,35 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.util.MappingOperations; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.util.Utils; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Path; public class InvertMappingsCommand extends Command { public InvertMappingsCommand() { super(Argument.INPUT_MAPPINGS.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), - Argument.OUTPUT_FOLDER.required()); + Argument.OUTPUT_FOLDER.required(), + Argument.OBFUSCATED_NAMESPACE.optional(), + Argument.DEOBFUSCATED_NAMESPACE.optional() + ); } @Override public void run(String... args) throws IOException, MappingParseException { Path source = getReadablePath(this.getArg(args, 0)); - String resultFormat = this.getArg(args, 1); Path result = getWritablePath(this.getArg(args, 2)); + String obfuscatedNamespace = this.getArg(args, 3); + String deobfuscatedNamespace = this.getArg(args, 4); - run(source, resultFormat, result); + run(source, result, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -39,15 +42,17 @@ public String getDescription() { return "Flips the source names with the destination names, ie. 'class a -> Example' becomes 'class Example -> a'."; } - public static void run(Path sourceFile, String resultFormat, Path resultFile) throws MappingParseException, IOException { - MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); - MappingFormat format = MappingFormat.parseFromFile(sourceFile); + public static void run(Path sourceFile, Path resultFile, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws MappingParseException, IOException { + MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, obfuscatedNamespace, deobfuscatedNamespace); + Enigma enigma = createEnigma(); - EntryTree source = format.read(sourceFile); + var readService = CommandsUtil.getReader(enigma, sourceFile); + var writeService = CommandsUtil.getWriter(enigma, resultFile); + + EntryTree source = readService.read(sourceFile); EntryTree result = MappingOperations.invert(source); Utils.delete(resultFile); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); - writer.write(result, resultFile, saveParameters); + writeService.write(result, resultFile, saveParameters); } } diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommand.java index 3f4c31e28..22aea0304 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommand.java @@ -1,14 +1,15 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.translation.MappingTranslator; import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; @@ -17,6 +18,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.util.Utils; +import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Path; import java.util.Map; @@ -25,18 +27,21 @@ public class MapSpecializedMethodsCommand extends Command { public MapSpecializedMethodsCommand() { super(Argument.INPUT_JAR.required(), Argument.INPUT_MAPPINGS.required(), - Argument.OUTPUT_MAPPING_FORMAT.required(), - Argument.MAPPING_OUTPUT.required()); + Argument.MAPPING_OUTPUT.required(), + Argument.OBFUSCATED_NAMESPACE.optional(), + Argument.DEOBFUSCATED_NAMESPACE.optional() + ); } @Override public void run(String... args) throws IOException, MappingParseException { Path jar = getReadablePath(this.getArg(args, 0)); Path source = getReadablePath(this.getArg(args, 1)); - String resultFormat = this.getArg(args, 2); - Path result = getWritablePath(this.getArg(args, 3)); + Path result = getWritablePath(this.getArg(args, 2)); + String obfuscatedNamespace = this.getArg(args, 3); + String deobfuscatedNamespace = this.getArg(args, 4); - run(jar, source, resultFormat, result); + run(jar, source, result, obfuscatedNamespace, deobfuscatedNamespace); } @Override @@ -49,18 +54,19 @@ public String getDescription() { return "Adds names for specialized methods from their corresponding bridge method"; } - public static void run(Path jar, Path sourcePath, String resultFormat, Path output) throws IOException, MappingParseException { + public static void run(Path jar, Path sourcePath, Path output, @Nullable String obfuscatedNamespace, @Nullable String deobfuscatedNamespace) throws IOException, MappingParseException { boolean debug = shouldDebug(new MapSpecializedMethodsCommand().getName()); JarIndex jarIndex = loadJar(jar); + Enigma enigma = createEnigma(); - MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); - MappingFormat sourceFormat = MappingFormat.parseFromFile(sourcePath); - EntryTree source = sourceFormat.read(sourcePath); + MappingSaveParameters saveParameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, obfuscatedNamespace, deobfuscatedNamespace); + MappingsReader reader = CommandsUtil.getReader(enigma, sourcePath); + EntryTree source = reader.read(sourcePath); EntryTree result = run(jarIndex, source, debug); Utils.delete(output); - MappingsWriter writer = MappingCommandsUtil.getWriter(resultFormat); + MappingsWriter writer = CommandsUtil.getWriter(enigma, output); writer.write(result, output, saveParameters); if (debug) { diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/MappingCommandsUtil.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/MappingCommandsUtil.java deleted file mode 100644 index 6fdd3bd67..000000000 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/MappingCommandsUtil.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.quiltmc.enigma.command; - -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; -import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Writer; - -public final class MappingCommandsUtil { - private MappingCommandsUtil() { - } - - public static MappingsWriter getWriter(String type) { - if (type.toLowerCase().startsWith("tinyv2:") || type.toLowerCase().startsWith("tiny_v2:")) { - String[] split = type.split(":"); - - if (split.length != 3) { - throw new IllegalArgumentException("specify column names as 'tinyv2:from_namespace:to_namespace'"); - } - - return new TinyV2Writer(split[1], split[2]); - } - - MappingFormat format; - try { - format = MappingFormat.valueOf(type); - } catch (IllegalArgumentException ignored) { - format = MappingFormat.valueOf(type.toUpperCase()); - } - - return format.getWriter(); - } -} diff --git a/enigma-cli/src/test/java/org/quiltmc/enigma/command/FillClassMappingsCommandTest.java b/enigma-cli/src/test/java/org/quiltmc/enigma/command/FillClassMappingsCommandTest.java index fbc08c60e..34300ea33 100644 --- a/enigma-cli/src/test/java/org/quiltmc/enigma/command/FillClassMappingsCommandTest.java +++ b/enigma-cli/src/test/java/org/quiltmc/enigma/command/FillClassMappingsCommandTest.java @@ -1,8 +1,9 @@ package org.quiltmc.enigma.command; import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; @@ -47,9 +48,10 @@ public class FillClassMappingsCommandTest extends CommandTest { @Test public void test() throws Exception { Path resultFile = Files.createTempFile("fillClassMappings", ".mappings"); - FillClassMappingsCommand.run(JAR, MAPPINGS, resultFile, MappingFormat.ENIGMA_FILE.name(), false); + FillClassMappingsCommand.run(JAR, MAPPINGS, resultFile, false, null, null); - EntryTree result = MappingFormat.ENIGMA_FILE.read(resultFile); + MappingsReader reader = CommandsUtil.getReader(Enigma.create(), resultFile); + EntryTree result = reader.read(resultFile); assertEquals("A_Anonymous", getName(result, A)); assertNotNull(result.findNode(A_ANONYMOUS)); diff --git a/enigma-cli/src/test/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommandTest.java b/enigma-cli/src/test/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommandTest.java index 2e1e01e2a..8161e4b47 100644 --- a/enigma-cli/src/test/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommandTest.java +++ b/enigma-cli/src/test/java/org/quiltmc/enigma/command/MapSpecializedMethodsCommandTest.java @@ -1,9 +1,10 @@ package org.quiltmc.enigma.command; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.TestUtil; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; @@ -60,9 +61,10 @@ public class MapSpecializedMethodsCommandTest extends CommandTest { @Test public void test() throws Exception { Path resultFile = Files.createTempFile("mapSpecializedMethods", ".mappings"); - MapSpecializedMethodsCommand.run(JAR, MAPPINGS, MappingFormat.ENIGMA_FILE.name(), resultFile); + MapSpecializedMethodsCommand.run(JAR, MAPPINGS, resultFile, null, null); - EntryTree result = MappingFormat.ENIGMA_FILE.read(resultFile, ProgressListener.createEmpty()); + MappingsReader reader = CommandsUtil.getReader(Enigma.create(), resultFile); + EntryTree result = reader.read(resultFile, ProgressListener.createEmpty()); assertNotNull(result.findNode(BASE_CLASS)); assertEquals("foo", getName(result, BASE_FOO_1)); diff --git a/enigma-server/build.gradle b/enigma-server/build.gradle index fe04e9059..d9c865b89 100644 --- a/enigma-server/build.gradle +++ b/enigma-server/build.gradle @@ -11,9 +11,10 @@ dependencies { testImplementation testFixtures(project(':enigma')) } -mainClassName = 'org.quiltmc.enigma.network.DedicatedEnigmaServer' - -jar.manifest.attributes 'Main-Class': mainClassName +application { + mainClass = 'org.quiltmc.enigma.network.DedicatedEnigmaServer' + jar.manifest.attributes 'Main-Class': mainClass +} publishing { publications { diff --git a/enigma-server/src/main/java/org/quiltmc/enigma/network/DedicatedEnigmaServer.java b/enigma-server/src/main/java/org/quiltmc/enigma/network/DedicatedEnigmaServer.java index 5a69fb13c..824cedeb6 100644 --- a/enigma-server/src/main/java/org/quiltmc/enigma/network/DedicatedEnigmaServer.java +++ b/enigma-server/src/main/java/org/quiltmc/enigma/network/DedicatedEnigmaServer.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.network; +import com.google.common.io.MoreFiles; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; @@ -9,8 +10,8 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.service.ReadWriteService; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; import org.quiltmc.enigma.util.Utils; @@ -21,6 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -28,7 +30,7 @@ public class DedicatedEnigmaServer extends EnigmaServer { private final EnigmaProfile profile; - private final MappingFormat mappingFormat; + private final ReadWriteService readWriteService; private final Path mappingsFile; private final PrintWriter log; private final BlockingQueue tasks = new LinkedBlockingDeque<>(); @@ -37,7 +39,7 @@ public DedicatedEnigmaServer( byte[] jarChecksum, char[] password, EnigmaProfile profile, - MappingFormat mappingFormat, + ReadWriteService readWriteService, Path mappingsFile, PrintWriter log, EntryRemapper mappings, @@ -45,7 +47,7 @@ public DedicatedEnigmaServer( ) { super(jarChecksum, password, mappings, port); this.profile = profile; - this.mappingFormat = mappingFormat; + this.readWriteService = readWriteService; this.mappingsFile = mappingsFile; this.log = log; } @@ -115,18 +117,22 @@ public static void main(String[] args) { Logger.info("Indexing Jar..."); EnigmaProject project = enigma.openJar(jar, new ClasspathClassProvider(), ProgressListener.createEmpty()); - MappingFormat mappingFormat = MappingFormat.parseFromFile(mappingsFile); + Optional readWriteService = enigma.getReadWriteService(mappingsFile); + if (readWriteService.isEmpty()) { + throw new IOException("Cannot read mapping file: unknown file type \"" + MoreFiles.getFileExtension(mappingsFile) + "\"!"); + } + EntryRemapper mappings; if (!Files.exists(mappingsFile)) { mappings = EntryRemapper.mapped(project.getJarIndex(), project.getMappingsIndex(), project.getRemapper().getJarProposedMappings(), new HashEntryTree<>(), enigma.getNameProposalServices()); } else { Logger.info("Reading mappings..."); - mappings = EntryRemapper.mapped(project.getJarIndex(), project.getMappingsIndex(), project.getRemapper().getJarProposedMappings(), mappingFormat.read(mappingsFile), enigma.getNameProposalServices()); + mappings = EntryRemapper.mapped(project.getJarIndex(), project.getMappingsIndex(), project.getRemapper().getJarProposedMappings(), readWriteService.get().read(mappingsFile), enigma.getNameProposalServices()); } PrintWriter log = new PrintWriter(Files.newBufferedWriter(logFile)); - server = new DedicatedEnigmaServer(checksum, password, profile, mappingFormat, mappingsFile, log, mappings, port); + server = new DedicatedEnigmaServer(checksum, password, profile, readWriteService.get(), mappingsFile, log, mappings, port); server.start(); Logger.info("Server started"); } catch (IOException | MappingParseException e) { @@ -154,7 +160,7 @@ public synchronized void stop() { } private void saveMappings() { - this.mappingFormat.write(this.getRemapper().getMappings(), this.getRemapper().takeMappingDelta(), this.mappingsFile, ProgressListener.createEmpty(), this.profile.getMappingSaveParameters()); + this.readWriteService.write(this.getRemapper().getMappings(), this.getRemapper().takeMappingDelta(), this.mappingsFile, ProgressListener.createEmpty(), this.profile.getMappingSaveParameters()); this.log.flush(); } diff --git a/enigma-swing/build.gradle b/enigma-swing/build.gradle index faaa6102e..37c93075d 100644 --- a/enigma-swing/build.gradle +++ b/enigma-swing/build.gradle @@ -19,12 +19,13 @@ dependencies { testImplementation(testFixtures(project(':enigma'))) } -mainClassName = 'org.quiltmc.enigma.gui.Main' - -jar.manifest.attributes 'Main-Class': mainClassName +application { + mainClass = 'org.quiltmc.enigma.gui.Main' + jar.manifest.attributes 'Main-Class': mainClass +} static String convertToGradleTaskName(String name) { - String newName = new String(name); + String newName = new String(name) for (int i = 0; i < name.length(); i++) { if (name.charAt(i) == '_') { @@ -38,13 +39,13 @@ static String convertToGradleTaskName(String name) { } def registerTestTask(String name) { - String taskName = convertToGradleTaskName(name); + String taskName = convertToGradleTaskName(name) tasks.register("${taskName}TestGui", JavaExec.class) { group("test") dependsOn(":enigma:${taskName}TestObf") dependsOn(":enigma:processResources") - mainClass = mainClassName + mainClass = application.mainClass classpath = sourceSets.test.runtimeClasspath def jar = project(":enigma").file("build/test-obf/${name}.jar") @@ -59,7 +60,7 @@ def registerTestTask(String name) { tasks.register("${taskName}TestGui2", JavaExec.class) { group("test") dependsOn(":enigma:${taskName}TestObf") - mainClass = mainClassName + mainClass = application.mainClass classpath = sourceSets.test.runtimeClasspath def jar = project(":enigma").file("build/test-obf/${name}.jar") diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java index e6ff0830c..89cc109af 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java @@ -29,6 +29,7 @@ import org.quiltmc.enigma.gui.panel.EditorPanel; import org.quiltmc.enigma.gui.panel.IdentifierPanel; import org.quiltmc.enigma.gui.renderer.MessageListCellRenderer; +import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; @@ -95,8 +96,7 @@ public class Gui { private final NotificationManager notificationManager; public final JFileChooser jarFileChooser; - public final JFileChooser tinyMappingsFileChooser; - public final JFileChooser enigmaMappingsFileChooser; + public final JFileChooser mappingsFileChooser; public final JFileChooser exportSourceFileChooser; public final JFileChooser exportJarFileChooser; public final SearchDialog searchDialog; @@ -118,8 +118,7 @@ public Gui(EnigmaProfile profile, Set editableTypes, boolean visib this.splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.centerPanel, this.dockerManager.getRightDock()); this.splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.dockerManager.getLeftDock(), this.splitRight); this.jarFileChooser = new JFileChooser(); - this.tinyMappingsFileChooser = new JFileChooser(); - this.enigmaMappingsFileChooser = new JFileChooser(); + this.mappingsFileChooser = new JFileChooser(); this.exportSourceFileChooser = new JFileChooser(); this.exportJarFileChooser = new JFileChooser(); this.connectionStatusLabel = new JLabel(); @@ -165,10 +164,6 @@ private void setupUi() { this.setupDockers(); this.jarFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - this.tinyMappingsFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - - this.enigmaMappingsFileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); - this.enigmaMappingsFileChooser.setAcceptAllFileFilterUsed(false); this.exportSourceFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); this.exportSourceFileChooser.setAcceptAllFileFilterUsed(false); @@ -367,7 +362,7 @@ public void updateAllClasses() { } public void setMappingsFile(Path path) { - this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); + this.mappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); this.updateUiState(); } @@ -491,8 +486,10 @@ public void showDiscardDiag(IntFunction callback, String... options) { } public CompletableFuture saveMapping() { - if (this.enigmaMappingsFileChooser.getSelectedFile() != null || this.enigmaMappingsFileChooser.showSaveDialog(this.mainWindow.getFrame()) == JFileChooser.APPROVE_OPTION) { - return this.controller.saveMappings(this.enigmaMappingsFileChooser.getSelectedFile().toPath()); + ExtensionFileFilter.setupFileChooser(this.getController().getGui(), this.mappingsFileChooser, this.controller.getReadWriteService()); + + if (this.mappingsFileChooser.getSelectedFile() != null || this.mappingsFileChooser.showSaveDialog(this.mainWindow.getFrame()) == JFileChooser.APPROVE_OPTION) { + return this.controller.saveMappings(ExtensionFileFilter.getSavePath(this.mappingsFileChooser)); } return CompletableFuture.completedFuture(null); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java index 15d285420..032ca0aab 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/GuiController.java @@ -12,6 +12,7 @@ import org.quiltmc.enigma.api.analysis.tree.ClassReferenceTreeNode; import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.tree.FieldReferenceTreeNode; +import org.quiltmc.enigma.api.service.ReadWriteService; import org.quiltmc.enigma.gui.dialog.CrashDialog; import org.quiltmc.enigma.gui.network.IntegratedEnigmaClient; import org.quiltmc.enigma.impl.analysis.IndexTreeBuilder; @@ -39,7 +40,7 @@ import org.quiltmc.enigma.network.packet.c2s.LoginC2SPacket; import org.quiltmc.enigma.network.packet.Packet; import org.quiltmc.enigma.api.source.DecompiledClassSource; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.SourceIndex; import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.stats.StatsGenerator; @@ -53,7 +54,6 @@ import org.quiltmc.enigma.util.EntryUtil; import org.quiltmc.enigma.api.translation.mapping.MappingDelta; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; @@ -70,6 +70,7 @@ import org.quiltmc.enigma.util.validation.ValidationContext; import org.tinylog.Logger; +import javax.annotation.Nullable; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import java.awt.Desktop; @@ -98,7 +99,7 @@ public class GuiController implements ClientPacketHandler { private StatsGenerator statsGenerator; private Path loadedMappingPath; - private MappingFormat loadedMappingFormat; + private ReadWriteService readWriteService; private ClassHandleProvider chp; @@ -145,10 +146,16 @@ public void closeJar() { } public CompletableFuture openMappings(Path path) { - return this.openMappings(MappingFormat.parseFromFile(path), path); + var readWriteService = this.enigma.getReadWriteService(path); + if (readWriteService.isEmpty() || !readWriteService.get().supportsReading()) { + Logger.error("Could not open mappings: no reader found for file \"{}\"", path); + return CompletableFuture.supplyAsync(() -> null); + } + + return this.openMappings(readWriteService.get(), path); } - public CompletableFuture openMappings(MappingFormat format, Path path) { + public CompletableFuture openMappings(ReadWriteService readWriteService, Path path) { if (this.project == null || !new File(path.toUri()).exists()) { return CompletableFuture.supplyAsync(() -> null); } @@ -159,10 +166,10 @@ public CompletableFuture openMappings(MappingFormat format, Path path) { return ProgressDialog.runOffThread(this.gui, progress -> { try { - EntryTree mappings = format.read(path); + EntryTree mappings = readWriteService.read(path); this.project.setMappings(mappings, progress); - this.loadedMappingFormat = format; + this.readWriteService = readWriteService; this.loadedMappingPath = path; this.refreshClasses(); @@ -191,7 +198,7 @@ public void openMappings(EntryTree mappings) { } public CompletableFuture saveMappings(Path path) { - return this.saveMappings(path, this.loadedMappingFormat); + return this.saveMappings(path, this.readWriteService); } /** @@ -202,14 +209,14 @@ public CompletableFuture saveMappings(Path path) { * join on the future in gui, but rather call {@code thenXxx} methods. * * @param path the path of the save - * @param format the format of the save + * @param service the writer for the mapping type * @return the future of saving */ - public CompletableFuture saveMappings(Path path, MappingFormat format) { + public CompletableFuture saveMappings(Path path, ReadWriteService service) { if (this.project == null) { return CompletableFuture.completedFuture(null); - } else if (format.getWriter() == null) { - String nonWriteableMessage = I18n.translateFormatted("menu.file.save.non_writeable", I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT))); + } else if (!service.supportsWriting()) { + String nonWriteableMessage = I18n.translateFormatted("menu.file.save.non_writeable", I18n.translate("mapping_format." + service.getId().toLowerCase(Locale.ROOT))); JOptionPane.showMessageDialog(this.gui.getFrame(), nonWriteableMessage, I18n.translate("menu.file.save.cannot_save"), JOptionPane.ERROR_MESSAGE); return CompletableFuture.completedFuture(null); } @@ -221,13 +228,13 @@ public CompletableFuture saveMappings(Path path, MappingFormat format) { MappingDelta delta = mapper.takeMappingDelta(); boolean saveAll = !path.equals(this.loadedMappingPath); - this.loadedMappingFormat = format; + this.readWriteService = service; this.loadedMappingPath = path; if (saveAll) { - format.write(mapper.getMappings(), path, progress, saveParameters); + service.write(mapper.getMappings(), path, progress, saveParameters); } else { - format.write(mapper.getMappings(), delta, path, progress, saveParameters); + service.write(mapper.getMappings(), delta, path, progress, saveParameters); } }); } @@ -247,16 +254,16 @@ public void reloadAll() { if (jarPath != null) { this.closeJar(); CompletableFuture f = this.openJar(jarPath); - if (this.loadedMappingFormat != null && this.loadedMappingPath != null) { - f.whenComplete((v, t) -> this.openMappings(this.loadedMappingFormat, this.loadedMappingPath)); + if (this.readWriteService != null && this.loadedMappingPath != null) { + f.whenComplete((v, t) -> this.openMappings(this.readWriteService, this.loadedMappingPath)); } } } public void reloadMappings() { - if (this.loadedMappingFormat != null && this.loadedMappingPath != null) { + if (this.readWriteService != null && this.loadedMappingPath != null) { this.closeMappings(); - this.openMappings(this.loadedMappingFormat, this.loadedMappingPath); + this.openMappings(this.readWriteService, this.loadedMappingPath); } } @@ -611,10 +618,16 @@ public Enigma getEnigma() { return this.enigma; } + @Nullable public StatsGenerator getStatsGenerator() { return this.statsGenerator; } + @Nullable + public ReadWriteService getReadWriteService() { + return this.readWriteService; + } + public void createClient(String username, String ip, int port, char[] password) throws IOException { this.client = new IntegratedEnigmaClient(this, ip, port); this.client.connect(); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Decompiler.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Decompiler.java index e66af2967..76ba3da26 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Decompiler.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/Decompiler.java @@ -2,7 +2,7 @@ import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.dialog.decompiler.VineflowerSettingsDialog; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Decompilers; import java.util.function.BiConsumer; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java index 4c9f9b386..53ccb984d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/MenuBar.java @@ -1,6 +1,6 @@ package org.quiltmc.enigma.gui.element; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.api.service.ReadWriteService; import org.quiltmc.enigma.gui.ConnectionState; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.NotificationManager; @@ -18,6 +18,7 @@ import org.quiltmc.enigma.gui.dialog.StatsDialog; import org.quiltmc.enigma.gui.dialog.decompiler.DecompilerSettingsDialog; import org.quiltmc.enigma.gui.dialog.keybind.ConfigureKeyBindsDialog; +import org.quiltmc.enigma.gui.util.ExtensionFileFilter; import org.quiltmc.enigma.gui.util.GuiUtil; import org.quiltmc.enigma.gui.util.LanguageUtil; import org.quiltmc.enigma.gui.util.ScaleUtil; @@ -42,6 +43,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -222,7 +224,7 @@ public void updateUiState() { this.jarCloseItem.setEnabled(jarOpen); this.openMappingsItem.setEnabled(jarOpen); - this.saveMappingsItem.setEnabled(jarOpen && this.gui.enigmaMappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED); + this.saveMappingsItem.setEnabled(jarOpen && this.gui.mappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED); this.saveMappingsAsMenu.setEnabled(jarOpen); this.closeMappingsItem.setEnabled(jarOpen); this.reloadMappingsItem.setEnabled(jarOpen); @@ -324,7 +326,7 @@ private void onMaxRecentFilesClicked() { } private void onSaveMappingsClicked() { - this.gui.getController().saveMappings(this.gui.enigmaMappingsFileChooser.getSelectedFile().toPath()); + this.gui.getController().saveMappings(this.gui.mappingsFileChooser.getSelectedFile().toPath()); } private void openMappingsDiscardPrompt(Runnable then) { @@ -469,16 +471,24 @@ private void onGithubClicked() { } private void onOpenMappingsClicked() { - this.gui.enigmaMappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); - if (this.gui.enigmaMappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { - File selectedFile = this.gui.enigmaMappingsFileChooser.getSelectedFile(); - Config.main().stats.lastSelectedDir.setValue(this.gui.enigmaMappingsFileChooser.getCurrentDirectory().toString(), true); - - MappingFormat format = MappingFormat.parseFromFile(selectedFile.toPath()); - if (format.getReader() != null) { - this.gui.getController().openMappings(format, selectedFile.toPath()); + this.gui.mappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + + List types = this.gui.getController().getEnigma().getReadWriteServices().stream().filter(ReadWriteService::supportsReading).toList(); + ExtensionFileFilter.setupFileChooser(this.gui, this.gui.mappingsFileChooser, types.toArray(new ReadWriteService[0])); + + if (this.gui.mappingsFileChooser.showOpenDialog(this.gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.gui.mappingsFileChooser.getSelectedFile(); + Config.main().stats.lastSelectedDir.setValue(this.gui.mappingsFileChooser.getCurrentDirectory().toString(), true); + + Optional format = this.gui.getController().getEnigma().getReadWriteService(selectedFile.toPath()); + if (format.isPresent() && format.get().supportsReading()) { + this.gui.getController().openMappings(format.get(), selectedFile.toPath()); } else { - String nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable", I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT))); + String nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable.unsupported_format", selectedFile); + if (format.isPresent()) { + nonParseableMessage = I18n.translateFormatted("menu.file.open.non_parseable", I18n.translate("mapping_format." + format.get().getId().toLowerCase(Locale.ROOT))); + } + JOptionPane.showMessageDialog(this.gui.getFrame(), nonParseableMessage, I18n.translate("menu.file.open.cannot_open"), JOptionPane.ERROR_MESSAGE); } } @@ -545,19 +555,22 @@ private static Path findCommonPath(Path a, Path b) { } private static void prepareSaveMappingsAsMenu(JMenu saveMappingsAsMenu, JMenuItem saveMappingsItem, Gui gui) { - for (MappingFormat format : MappingFormat.values()) { - if (format.getWriter() != null) { - JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.name().toLowerCase(Locale.ROOT))); + for (ReadWriteService format : gui.getController().getEnigma().getReadWriteServices()) { + if (format.supportsWriting()) { + JMenuItem item = new JMenuItem(I18n.translate("mapping_format." + format.getId().toLowerCase(Locale.ROOT))); item.addActionListener(event -> { - // TODO: Use a specific file chooser for it - if (gui.enigmaMappingsFileChooser.getCurrentDirectory() == null) { - gui.enigmaMappingsFileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); + JFileChooser fileChooser = gui.mappingsFileChooser; + ExtensionFileFilter.setupFileChooser(gui, fileChooser, format); + + if (fileChooser.getCurrentDirectory() == null) { + fileChooser.setCurrentDirectory(new File(Config.main().stats.lastSelectedDir.value())); } - if (gui.enigmaMappingsFileChooser.showSaveDialog(gui.getFrame()) == JFileChooser.APPROVE_OPTION) { - gui.getController().saveMappings(gui.enigmaMappingsFileChooser.getSelectedFile().toPath(), format); + if (fileChooser.showSaveDialog(gui.getFrame()) == JFileChooser.APPROVE_OPTION) { + Path savePath = ExtensionFileFilter.getSavePath(fileChooser); + gui.getController().saveMappings(savePath, format); saveMappingsItem.setEnabled(true); - Config.main().stats.lastSelectedDir.setValue(gui.enigmaMappingsFileChooser.getCurrentDirectory().toString(), true); + Config.main().stats.lastSelectedDir.setValue(fileChooser.getCurrentDirectory().toString()); } }); saveMappingsAsMenu.add(item); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ExtensionFileFilter.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ExtensionFileFilter.java new file mode 100644 index 000000000..d460ef842 --- /dev/null +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ExtensionFileFilter.java @@ -0,0 +1,136 @@ +package org.quiltmc.enigma.gui.util; + +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.gui.Gui; +import org.quiltmc.enigma.util.I18n; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +public final class ExtensionFileFilter extends FileFilter { + private final String formatName; + private final List extensions; + + /** + * Constructs an {@code ExtensionFileFilter}. + * + * @param formatName the human-readable name of the file format + * @param extensions the file extensions with no leading dots + */ + public ExtensionFileFilter(String formatName, List extensions) { + this.formatName = formatName; + this.extensions = extensions.stream().peek(s -> { + if (s.startsWith(".")) { + throw new IllegalArgumentException("extensions cannot start with dots!"); + } + }).map(s -> "." + s).toList(); + } + + public List getExtensions() { + return this.extensions; + } + + @Override + public boolean accept(File f) { + // Always accept directories so the user can see them. + if (f.isDirectory()) { + return true; + } + + for (String extension : this.extensions) { + if (f.getName().endsWith(extension)) { + return true; + } + } + + return false; + } + + @Override + public String getDescription() { + var joiner = new StringJoiner(", "); + + for (String extension : this.extensions) { + joiner.add("*" + extension); + } + + return I18n.translateFormatted("menu.file.mappings.file_filter", this.formatName, joiner.toString()); + } + + /** + * Sets up a file chooser with a mapping format. This method resets the choosable filters, + * and adds and selects a new filter based on the provided mapping format. + * + * @param fileChooser the file chooser to set up + * @param services the read/write services to use. if empty, defaults to the current writer + */ + public static void setupFileChooser(Gui gui, JFileChooser fileChooser, ReadWriteService... services) { + if (services.length == 0) { + services = new ReadWriteService[]{gui.getController().getReadWriteService()}; + } + + // Remove previous custom filters. + fileChooser.resetChoosableFileFilters(); + + for (ReadWriteService service : services) { + if (service.getFileType().isDirectory()) { + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + } else { + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + String formatName = I18n.translate("mapping_format." + service.getId().toLowerCase()); + var filter = new ExtensionFileFilter(formatName, service.getFileType().getExtensions()); + // Add our new filter to the list... + fileChooser.addChoosableFileFilter(filter); + // ...and choose it as the default. + fileChooser.setFileFilter(filter); + } + } + + if (services.length > 1) { + List extensions = Arrays.stream(services).flatMap(format -> format.getFileType().getExtensions().stream()).distinct().toList(); + var filter = new ExtensionFileFilter(I18n.translate("mapping_format.all_formats"), extensions); + fileChooser.addChoosableFileFilter(filter); + fileChooser.setFileFilter(filter); + } + } + + /** + * Fixes a missing file extension in a save file path when the selected filter + * is an {@code ExtensionFileFilter}. + * + * @param fileChooser the file chooser to check + * @return the fixed path + */ + public static Path getSavePath(JFileChooser fileChooser) { + Path savePath = fileChooser.getSelectedFile().toPath(); + + if (fileChooser.getFileFilter() instanceof ExtensionFileFilter extensionFilter) { + // Check that the file name ends with the extension. + String fileName = savePath.getFileName().toString(); + boolean hasExtension = false; + + for (String extension : extensionFilter.getExtensions()) { + if (fileName.endsWith(extension)) { + hasExtension = true; + break; + } + } + + if (!hasExtension) { + String defaultExtension = extensionFilter.getExtensions().get(0); + // If not, add the extension. + savePath = savePath.resolveSibling(fileName + defaultExtension); + // Store the adjusted file, so that it shows up properly + // the next time this dialog is used. + fileChooser.setSelectedFile(savePath.toFile()); + } + } + + return savePath; + } +} diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/LanguageChangeListener.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/LanguageChangeListener.java index 818a909b8..805888d73 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/LanguageChangeListener.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/LanguageChangeListener.java @@ -2,8 +2,4 @@ public interface LanguageChangeListener { void retranslateUi(); - - default boolean isValid() { - return true; - } } diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ProgressBar.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ProgressBar.java index f6f975b7d..1565480e3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ProgressBar.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/util/ProgressBar.java @@ -47,7 +47,7 @@ public void init(int totalWork, String title) { @Override public void step(int workDone, String message) { super.step(workDone, message); - this.messageUpdateListeners.forEach(listener -> listener.accept(message, workDone == totalWork)); + this.messageUpdateListeners.forEach(listener -> listener.accept(message, workDone == this.totalWork)); SwingUtilities.invokeLater(() -> { if (workDone != -1) { diff --git a/enigma-swing/src/test/java/org/quiltmc/enigma/TestPackageRename.java b/enigma-swing/src/test/java/org/quiltmc/enigma/TestPackageRename.java index c7805db3b..9329f8306 100644 --- a/enigma-swing/src/test/java/org/quiltmc/enigma/TestPackageRename.java +++ b/enigma-swing/src/test/java/org/quiltmc/enigma/TestPackageRename.java @@ -8,7 +8,6 @@ import org.quiltmc.enigma.gui.util.PackageRenamer; import org.quiltmc.enigma.api.translation.TranslateResult; import org.quiltmc.enigma.api.translation.Translator; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -122,7 +121,7 @@ private static ClassSelectorPopupMenu setupMenu() throws InterruptedException { gui.setShowsProgressBars(false); CountDownLatch latch = new CountDownLatch(1); - gui.getController().openJar(JAR).thenRun(() -> gui.getController().openMappings(MappingFormat.ENIGMA_DIRECTORY, MAPPINGS).thenRun(latch::countDown)); + gui.getController().openJar(JAR).thenRun(() -> gui.getController().openMappings(MAPPINGS).thenRun(latch::countDown)); latch.await(); deobfuscator = gui.getController().getProject().getRemapper().getDeobfuscator(); 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 9bfe81798..6599d153b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.api; +import com.google.common.io.MoreFiles; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; import org.quiltmc.enigma.api.service.EnigmaService; @@ -13,8 +14,10 @@ import org.quiltmc.enigma.api.class_provider.JarClassProvider; import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.service.ReadWriteService; import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; import org.quiltmc.enigma.api.translation.representation.entry.Entry; @@ -24,9 +27,16 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableListMultimap; import org.objectweb.asm.Opcodes; +import org.tinylog.Logger; +import javax.annotation.Nullable; +import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,6 +45,7 @@ import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; public class Enigma { public static final String NAME = "Enigma"; @@ -128,6 +139,96 @@ public List getNameProposalServices() { return proposalServices; } + /** + * {@return all registered read/write services} + */ + public List getReadWriteServices() { + return this.services.get(ReadWriteService.TYPE); + } + + /** + * Gets all supported {@link FileType file types} for reading or writing. + * @return the list of supported file types + */ + public List getSupportedFileTypes() { + return this.getReadWriteServices().stream().map(ReadWriteService::getFileType).toList(); + } + + /** + * Gets the {@link ReadWriteService read/write service} for the provided {@link FileType file type}. + * @param fileType the file type to get the service for + * @return the read/write service for the file type + */ + public Optional getReadWriteService(FileType fileType) { + return this.getReadWriteServices().stream().filter(service -> service.getFileType().equals(fileType)).findFirst(); + } + + /** + * Parses the {@link FileType file type} of the provided path and returns the corresponding {@link ReadWriteService read/write service} + * @param path the path to analyse + * @return the read/write service for the file type of the path + */ + public Optional getReadWriteService(Path path) { + return this.parseFileType(path).flatMap(this::getReadWriteService); + } + + /** + * Determines the mapping format of the provided path. Checks all formats according to their {@link FileType} file extensions. + * If the path is a directory, it will check the first file in the directory. For directories, defaults to the enigma mappings format. + * @param path the path to analyse + * @return the mapping format of the path + */ + public Optional parseFileType(Path path) { + List supportedTypes = this.getSupportedFileTypes(); + + if (Files.isDirectory(path)) { + try { + final AtomicReference> firstFile = new AtomicReference<>(); + firstFile.set(Optional.empty()); + + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + super.visitFile(file, attrs); + if (attrs.isRegularFile()) { + firstFile.set(Optional.of(file.toFile())); + return FileVisitResult.TERMINATE; + } + + return FileVisitResult.CONTINUE; + } + }); + + if (firstFile.get().isPresent()) { + for (FileType type : supportedTypes) { + if (!type.isDirectory()) { + continue; + } + + String extension = MoreFiles.getFileExtension(firstFile.get().get().toPath()).toLowerCase(); + if (type.getExtensions().contains(extension)) { + return Optional.of(type); + } + } + } + + return this.getSupportedFileTypes().stream().filter(type -> type.isDirectory() && type.getExtensions().contains("mapping")).findFirst(); + } catch (Exception e) { + Logger.error(e, "Failed to determine mapping format of directory {}", path); + } + } else { + String extension = MoreFiles.getFileExtension(path).toLowerCase(); + + for (FileType type : supportedTypes) { + if (type.getExtensions().contains(extension)) { + return Optional.of(type); + } + } + } + + return Optional.empty(); + } + public static class Builder { private EnigmaProfile profile = EnigmaProfile.EMPTY; private Iterable plugins = ServiceLoader.load(EnigmaPlugin.class); @@ -171,20 +272,30 @@ private static class PluginContext implements EnigmaPluginContext { public void registerService(EnigmaServiceType serviceType, EnigmaServiceFactory factory) { List serviceProfiles = this.profile.getServiceProfiles(serviceType); + if (serviceProfiles.isEmpty() && serviceType.activeByDefault()) { + this.putService(serviceType, factory.create(this.getServiceContext(null))); + return; + } + for (EnigmaProfile.Service serviceProfile : serviceProfiles) { T service = factory.create(this.getServiceContext(serviceProfile)); if (serviceProfile.matches(service.getId())) { - this.services.put(serviceType, service); + this.putService(serviceType, service); break; } } } - private EnigmaServiceContext getServiceContext(EnigmaProfile.Service serviceProfile) { + private void putService(EnigmaServiceType serviceType, EnigmaService service) { + this.validateRegistration(this.services.build(), serviceType, service); + this.services.put(serviceType, service); + } + + private EnigmaServiceContext getServiceContext(@Nullable EnigmaProfile.Service serviceProfile) { return new EnigmaServiceContext<>() { @Override public Optional>> getArgument(String key) { - return serviceProfile.getArgument(key); + return serviceProfile == null ? Optional.empty() : serviceProfile.getArgument(key); } @Override @@ -204,6 +315,11 @@ EnigmaServices buildServices() { for (EnigmaServiceType type : builtServices.keySet()) { List serviceProfiles = this.profile.getServiceProfiles(type); + if (serviceProfiles.isEmpty() && type.activeByDefault()) { + orderedServices.putAll(type, builtServices.get(type)); + continue; + } + for (EnigmaProfile.Service service : serviceProfiles) { for (EnigmaService registeredService : builtServices.get(type)) { if (service.matches(registeredService.getId())) { @@ -214,7 +330,36 @@ EnigmaServices buildServices() { } } - return new EnigmaServices(orderedServices.build()); + var builtOrderedServices = orderedServices.build(); + return new EnigmaServices(builtOrderedServices); + } + + private void validateRegistration(ImmutableListMultimap, EnigmaService> services, EnigmaServiceType serviceType, EnigmaService service) { + this.validatePluginId(service.getId()); + + for (EnigmaService otherService : services.get(serviceType)) { + // all services + if (service.getId().equals(otherService.getId())) { + throw new IllegalStateException("Multiple services of type " + serviceType + " have the same ID: \"" + service.getId() + "\""); + } + + // read write services + if (service instanceof ReadWriteService rwService + && otherService instanceof ReadWriteService otherRwService + && rwService.getFileType().isDirectory() == otherRwService.getFileType().isDirectory()) { + for (String extension : rwService.getFileType().getExtensions()) { + if (otherRwService.getFileType().getExtensions().contains(extension)) { + throw new IllegalStateException("Multiple read/write services found supporting the same extension: " + extension + " (id: " + service.getId() + ", other id: " + otherService.getId() + ")"); + } + } + } + } + } + + private void validatePluginId(String id) { + if (!id.matches("([a-z0-9_]+):([a-z0-9_]+((/[a-z0-9_]+)+)?)")) { + throw new IllegalArgumentException("Invalid plugin id: \"" + id + "\"\n" + "Refer to Javadoc on EnigmaService#getId for how to properly form a service ID."); + } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaPlugin.java index 6b7a3417a..c98f5fd39 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaPlugin.java @@ -1,5 +1,11 @@ package org.quiltmc.enigma.api; +/** + * An enigma plugin represents a collection of {@link org.quiltmc.enigma.api.service.EnigmaService services} that perform different functions. + */ public interface EnigmaPlugin { + /** + * Initializes the plugin, registering all services. + */ void init(EnigmaPluginContext ctx); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProfile.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProfile.java index 4ab1a121b..178c5a274 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProfile.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProfile.java @@ -33,7 +33,7 @@ public final class EnigmaProfile { public static final EnigmaProfile EMPTY = new EnigmaProfile(new ServiceContainer(Map.of())); - private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, null, null); private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(ServiceContainer.class, (JsonDeserializer) EnigmaProfile::loadServiceContainer) .create(); @@ -96,11 +96,11 @@ private static ServiceContainer loadServiceContainer(JsonElement json, Type type } public List getServiceProfiles(EnigmaServiceType serviceType) { - return this.serviceProfiles.get(serviceType.key); + return this.serviceProfiles.get(serviceType.key()); } public MappingSaveParameters getMappingSaveParameters() { - //noinspection ConstantConditions + //noinspection ConstantConditions - this field is parsed by GSON return this.mappingSaveParameters == null ? EnigmaProfile.DEFAULT_MAPPING_SAVE_PARAMETERS : this.mappingSaveParameters; } @@ -109,6 +109,12 @@ private EnigmaProfile withSourcePath(Path sourcePath) { return this; } + /** + * Resolves the path relative to the parent directory of this profile. + * If the profile was not loaded from a file, the path is returned as-is. + * @param path the path to resolve + * @return the resolved path + */ public Path resolvePath(Path path) { if (this.sourcePath == null) { return path; 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 d7c3a371c..09a9204d2 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -15,7 +15,7 @@ import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; import org.quiltmc.enigma.api.source.Decompiler; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.SourceSettings; import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/class_handle/ClassHandleProvider.java b/enigma/src/main/java/org/quiltmc/enigma/api/class_handle/ClassHandleProvider.java index 1a686ecff..bff160b36 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/class_handle/ClassHandleProvider.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/class_handle/ClassHandleProvider.java @@ -7,7 +7,7 @@ import org.quiltmc.enigma.api.event.ClassHandleListener; import org.quiltmc.enigma.api.source.DecompiledClassSource; import org.quiltmc.enigma.api.source.Decompiler; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Source; import org.quiltmc.enigma.api.source.SourceIndex; import org.quiltmc.enigma.api.source.SourceSettings; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/DecompilerService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/DecompilerService.java new file mode 100644 index 000000000..fa5c4092a --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/DecompilerService.java @@ -0,0 +1,16 @@ +package org.quiltmc.enigma.api.service; + +import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.source.Decompiler; +import org.quiltmc.enigma.api.source.SourceSettings; + +/** + * Decompiler services provide implementations of {@link Decompiler} in order to convert bytecode into human-readable source code. + *
+ * Decompiler services are active by default, and as such do not need to be specified in the {@link org.quiltmc.enigma.api.EnigmaProfile profile}. + */ +public interface DecompilerService extends EnigmaService { + EnigmaServiceType TYPE = EnigmaServiceType.create("decompiler", true); + + Decompiler create(ClassProvider classProvider, SourceSettings settings); +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaService.java index 93dc776ff..45c0de7be 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaService.java @@ -1,5 +1,8 @@ package org.quiltmc.enigma.api.service; +/** + * An enigma service is a component that provides a specific feature or piece of functionality to enigma. + */ public interface EnigmaService { /** * The ID of this service. This should satisfy a few criteria: @@ -8,7 +11,7 @@ public interface EnigmaService { *
  • Be all lowercase, with words separated by underscores. Slashes are allowed only after the namespace.
  • *
  • Be constant and unique: it should never change.
  • * - *

    Examples: {@code enigma:cfr}, {@code enigma:enum_name_proposer}, {@code your_plugin:custom_indexer}

    + *

    Examples: {@code enigma:cfr}, {@code enigma:enum_proposers/name}, {@code your_plugin:custom_indexer}

    * * @return the constant ID */ diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceContext.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceContext.java index c562bebf5..2c02e80bf 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceContext.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceContext.java @@ -21,6 +21,12 @@ default Optional> getMultipleArguments(String key) { return this.getArgument(key).flatMap(e -> e.right()); } + /** + * Resolves the path relative to the parent directory of the loaded {@link org.quiltmc.enigma.api.EnigmaProfile enigma profile}. + * If there is no profile or the profile was not loaded from a file, the path is returned as-is. + * @param path the path to resolve + * @return the resolved path + */ default Path getPath(String path) { return Path.of(path); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceType.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceType.java index 0c7d08f72..a5be33bb4 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceType.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/EnigmaServiceType.java @@ -1,14 +1,37 @@ package org.quiltmc.enigma.api.service; -public final class EnigmaServiceType { - public final String key; - - private EnigmaServiceType(String key) { - this.key = key; +public record EnigmaServiceType( + String key, + boolean activeByDefault +) { + public static EnigmaServiceType create(String key, boolean activeByDefault) { + return new EnigmaServiceType<>(key, activeByDefault); } public static EnigmaServiceType create(String key) { - return new EnigmaServiceType<>(key); + return new EnigmaServiceType<>(key, false); + } + + /** + * The unique key of this service type. + */ + @Override + public String key() { + return this.key; + } + + /** + * Whether this service type is active by default. + * If {@code true}, this service type will be active without being explicitly enabled in the profile. + */ + @Override + public boolean activeByDefault() { + return this.activeByDefault; + } + + @Override + public String toString() { + return this.key; } @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java index d1d3a03af..12c79ce72 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java @@ -7,6 +7,11 @@ import java.util.Set; +/** + * Jar indexer services analyse jar files as they're opened to collect information about their contents. + *
    + * Jar indexer services are not active by default, and need to be specified in the {@link org.quiltmc.enigma.api.EnigmaProfile profile}. + */ public interface JarIndexerService extends EnigmaService { EnigmaServiceType TYPE = EnigmaServiceType.create("jar_indexer"); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java index 094d46928..a00ae418a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java @@ -11,6 +11,8 @@ /** * A name proposal service suggests default names for entries based on context from their types and surrounding mappings. + *
    + * Name proposal services are not active by default, and need to be specified in the {@link org.quiltmc.enigma.api.EnigmaProfile profile}. */ public interface NameProposalService extends EnigmaService { EnigmaServiceType TYPE = EnigmaServiceType.create("name_proposal"); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/ObfuscationTestService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/ObfuscationTestService.java index 3783f8b56..ca883773d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/ObfuscationTestService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/ObfuscationTestService.java @@ -2,6 +2,11 @@ import org.quiltmc.enigma.api.translation.representation.entry.Entry; +/** + * An obfuscation test service allows a plugin to override the deobfuscation status of an entry. + *
    + * Obfuscation test services are not active by default, and need to be specified in the {@link org.quiltmc.enigma.api.EnigmaProfile profile}. + */ public interface ObfuscationTestService extends EnigmaService { EnigmaServiceType TYPE = EnigmaServiceType.create("obfuscation_test"); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/ReadWriteService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/ReadWriteService.java new file mode 100644 index 000000000..9cdfe09f3 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/ReadWriteService.java @@ -0,0 +1,85 @@ +package org.quiltmc.enigma.api.service; + +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.MappingDelta; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsReader; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingsWriter; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Path; + +/** + * A read/write service defines a reader and/or a writer for mappings. + *
    + * The service is keyed by a {@link FileType file type}, which is specified by a file extension. There should be no more than one read/write service per file type. + *
    + * Read/write services are active by default, and as such do not need to be specified in the {@link org.quiltmc.enigma.api.EnigmaProfile profile}. + */ +public interface ReadWriteService extends EnigmaService, MappingsWriter, MappingsReader { + EnigmaServiceType TYPE = EnigmaServiceType.create("read_write", true); + + /** + * A unique file type for this service to read/write. + * This can either represent a directory or a single file. + * @return the file type + */ + FileType getFileType(); + + /** + * {@return whether this service supports reading mappings} + */ + boolean supportsReading(); + + /** + * {@return whether this service supports writing mappings} + */ + boolean supportsWriting(); + + static ReadWriteService create(@Nullable MappingsReader reader, @Nullable MappingsWriter writer, FileType fileType, String id) { + return new ReadWriteService() { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + if (writer == null) { + throw new UnsupportedOperationException("This service does not support writing!"); + } + + writer.write(mappings, delta, path, progress, saveParameters); + } + + @Override + public EntryTree read(Path path, ProgressListener progress) throws MappingParseException, IOException { + if (reader == null) { + throw new UnsupportedOperationException("This service does not support reading!"); + } + + return reader.read(path, progress); + } + + @Override + public boolean supportsReading() { + return reader != null; + } + + @Override + public boolean supportsWriting() { + return writer != null; + } + + @Override + public String getId() { + return id; + } + + @Override + public FileType getFileType() { + return fileType; + } + }; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompilerService.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompilerService.java deleted file mode 100644 index a9d3030cb..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompilerService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.quiltmc.enigma.api.source; - -import org.quiltmc.enigma.api.class_provider.ClassProvider; -import org.quiltmc.enigma.api.service.EnigmaService; -import org.quiltmc.enigma.api.service.EnigmaServiceType; - -public interface DecompilerService extends EnigmaService { - EnigmaServiceType TYPE = EnigmaServiceType.create("decompiler"); - - Decompiler create(ClassProvider classProvider, SourceSettings settings); -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/Decompilers.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/Decompilers.java index 62eeec5c6..1c61a2779 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/Decompilers.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/Decompilers.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.api.source; import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.impl.source.bytecode.BytecodeDecompiler; import org.quiltmc.enigma.impl.source.cfr.CfrDecompiler; import org.quiltmc.enigma.impl.source.procyon.ProcyonDecompiler; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/FileType.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/FileType.java new file mode 100644 index 000000000..dac25dadf --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/FileType.java @@ -0,0 +1,48 @@ +package org.quiltmc.enigma.api.translation.mapping.serde; + +import java.util.List; + +/** + * A file type. It can be either a single file with an extension, or a directory. + * + *

    If a file type has multiple extensions, the default for saving will be the first one. + */ +public interface FileType { + /** + * Gets all possible extensions for this type of mapping file. + * If {@link #isDirectory()} is {@code true}, this will return the types of mapping allowed inside the directory. + * @return the file extension options + */ + List getExtensions(); + + /** + * {@return whether this file type is a directory} + */ + boolean isDirectory(); + + record Directory(File file) implements FileType { + @Override + public List getExtensions() { + return this.file.getExtensions(); + } + + public boolean isDirectory() { + return true; + } + } + + record File(List extensions) implements FileType { + public File(String... extensions) { + this(List.of(extensions)); + } + + @Override + public List getExtensions() { + return this.extensions; + } + + public boolean isDirectory() { + return false; + } + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFileNameFormat.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFileNameFormat.java index 93449b13d..2e1b5a005 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFileNameFormat.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFileNameFormat.java @@ -2,9 +2,20 @@ import com.google.gson.annotations.SerializedName; +/** + * Defines a strategy for naming files in directory-based mapping formats. + */ public enum MappingFileNameFormat { + /** + * Names files based on the mappings' obfuscated names. + * Example: if a class is mapped from {@code a} to {@code GreatClass}, its file will be named {@code GreatClass}. + */ @SerializedName("by_obf") BY_OBF, + /** + * Names files based on the mappings' deobfuscated names. + * Example: if a class is mapped from {@code a} to {@code GreatClass}, its file will be named {@code a}. + */ @SerializedName("by_deobf") BY_DEOBF } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFormat.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFormat.java deleted file mode 100644 index 6d995b4c3..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingFormat.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.quiltmc.enigma.api.translation.mapping.serde; - -import com.google.common.io.MoreFiles; -import org.quiltmc.enigma.api.ProgressListener; -import org.quiltmc.enigma.api.translation.mapping.EntryMapping; -import org.quiltmc.enigma.api.translation.mapping.MappingDelta; -import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsReader; -import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsWriter; -import org.quiltmc.enigma.api.translation.mapping.serde.proguard.ProguardMappingsReader; -import org.quiltmc.enigma.api.translation.mapping.serde.srg.SrgMappingsWriter; -import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Reader; -import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Writer; -import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public enum MappingFormat { - ENIGMA_FILE(EnigmaMappingsWriter.FILE, EnigmaMappingsReader.FILE), - ENIGMA_DIRECTORY(EnigmaMappingsWriter.DIRECTORY, EnigmaMappingsReader.DIRECTORY), - ENIGMA_ZIP(EnigmaMappingsWriter.ZIP, EnigmaMappingsReader.ZIP), - TINY_V2(new TinyV2Writer("intermediary", "named"), new TinyV2Reader()), - SRG_FILE(SrgMappingsWriter.INSTANCE, null), - PROGUARD(null, ProguardMappingsReader.INSTANCE); - - private final MappingsWriter writer; - private final MappingsReader reader; - - MappingFormat(MappingsWriter writer, MappingsReader reader) { - this.writer = writer; - this.reader = reader; - } - - public void write(EntryTree mappings, Path path, MappingSaveParameters saveParameters) { - this.write(mappings, MappingDelta.added(mappings), path, ProgressListener.createEmpty(), saveParameters); - } - - public void write(EntryTree mappings, Path path, ProgressListener progressListener, MappingSaveParameters saveParameters) { - this.write(mappings, MappingDelta.added(mappings), path, progressListener, saveParameters); - } - - public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progressListener, MappingSaveParameters saveParameters) { - if (this.writer == null) { - throw new IllegalStateException(this.name() + " does not support writing"); - } - - this.writer.write(mappings, delta, path, progressListener, saveParameters); - } - - /** - * Reads the provided path and returns it as a tree of mappings. - * @param path the path to read - * @return a tree of every read mapping - * @throws IOException when there's an I/O error reading the files - * @throws MappingParseException when there's an issue with the content of a mapping file - */ - public EntryTree read(Path path) throws IOException, MappingParseException { - if (this.reader == null) { - throw new IllegalStateException(this.name() + " does not support reading"); - } - - return this.reader.read(path, ProgressListener.createEmpty()); - } - - /** - * Reads the provided path and returns it as a tree of mappings. - * @param path the path to read - * @param progressListener a progress listener to be used for displaying the progress to the user - * @return a tree of every read mapping - * @throws IOException when there's an I/O error reading the files - * @throws MappingParseException when there's an issue with the content of a mapping file - */ - public EntryTree read(Path path, ProgressListener progressListener) throws IOException, MappingParseException { - if (this.reader == null) { - throw new IllegalStateException(this.name() + " does not support reading"); - } - - return this.reader.read(path, progressListener); - } - - @Nullable - public MappingsWriter getWriter() { - return this.writer; - } - - @Nullable - public MappingsReader getReader() { - return this.reader; - } - - /** - * Determines the mapping format of the provided file. Checks all formats, and returns {@link #PROGUARD} if none match. - * @param file the file to analyse - * @apiNote Any directory is considered to be the {@link #ENIGMA_DIRECTORY} format. - * Proguard does not have an explicit file extension, so it is the fallback. - * @return the mapping format of the file. - */ - public static MappingFormat parseFromFile(Path file) { - if (Files.isDirectory(file)) { - return ENIGMA_DIRECTORY; - } else { - switch (MoreFiles.getFileExtension(file).toLowerCase()) { - case "zip" -> { - return ENIGMA_ZIP; - } - case "mapping", "mappings" -> { - return ENIGMA_FILE; - } - case "tiny" -> { - return TINY_V2; - } - case "tsrg" -> { - return SRG_FILE; - } - default -> { - return PROGUARD; - } - } - } - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingSaveParameters.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingSaveParameters.java index 54da2b6b4..86a07af59 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingSaveParameters.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/MappingSaveParameters.java @@ -2,7 +2,17 @@ import com.google.gson.annotations.SerializedName; -public record MappingSaveParameters(@SerializedName("file_name_format") MappingFileNameFormat fileNameFormat, @SerializedName("write_proposed_names") boolean writeProposedNames) { +import javax.annotation.Nullable; + +public record MappingSaveParameters( + @SerializedName("file_name_format") MappingFileNameFormat fileNameFormat, + @SerializedName("write_proposed_names") boolean writeProposedNames, + @SerializedName("obfuscated_namespace") @Nullable String obfuscatedNamespace, + @SerializedName("deobfuscated_namespace") @Nullable String deobfuscatedNamespace +) { + /** + * Controls how individual files will be named in directory-based mapping formats. + */ @Override public MappingFileNameFormat fileNameFormat() { return this.fileNameFormat; @@ -17,4 +27,22 @@ public MappingFileNameFormat fileNameFormat() { public boolean writeProposedNames() { return this.writeProposedNames; } + + /** + * The namespace of the mappings' obfuscated names. This will be saved in certain mapping formats. + * If null, the format will define a default namespace. + */ + @Override + public String obfuscatedNamespace() { + return this.obfuscatedNamespace; + } + + /** + * The namespace of the mappings' deobfuscated names. This will be saved in certain mapping formats. + * If null, the format will define a default namespace. + */ + @Override + public String deobfuscatedNamespace() { + return this.deobfuscatedNamespace; + } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Reader.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Reader.java index d447da5eb..8253b8eb8 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Reader.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Reader.java @@ -26,6 +26,7 @@ import java.util.List; public final class TinyV2Reader implements MappingsReader { + public static final MappingsReader INSTANCE = new TinyV2Reader(); private static final String MINOR_VERSION = "0"; // 0 indent private static final int IN_HEADER = 0; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java index f031196dd..24ace2f4d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java @@ -29,13 +29,10 @@ import java.util.stream.StreamSupport; public final class TinyV2Writer implements MappingsWriter { + public static final MappingsWriter INSTANCE = new TinyV2Writer(); private static final String MINOR_VERSION = "0"; - private final String obfHeader; - private final String deobfHeader; - public TinyV2Writer(String obfHeader, String deobfHeader) { - this.obfHeader = obfHeader; - this.deobfHeader = deobfHeader; + private TinyV2Writer() { } private static int getEntryKind(Entry e) { @@ -78,13 +75,23 @@ private static Comparator> mappingComparator() { @Override public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters parameters) { + String obfNamespace = parameters.obfuscatedNamespace(); + if (obfNamespace == null) { + obfNamespace = "obfuscated"; + } + + String deobfNamespace = parameters.deobfuscatedNamespace(); + if (deobfNamespace == null) { + deobfNamespace = "deobfuscated"; + } + List> classes = StreamSupport.stream(mappings.spliterator(), false) .filter(node -> node.getEntry() instanceof ClassEntry) .sorted(mappingComparator()) .toList(); try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(path))) { - writer.println("tiny\t2\t" + MINOR_VERSION + "\t" + this.obfHeader + "\t" + this.deobfHeader); + writer.println("tiny\t2\t" + MINOR_VERSION + "\t" + obfNamespace + "\t" + deobfNamespace); // no escape names diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/BuiltinPlugin.java deleted file mode 100644 index f0c9499fd..000000000 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/BuiltinPlugin.java +++ /dev/null @@ -1,210 +0,0 @@ -package org.quiltmc.enigma.impl.analysis; - -import org.quiltmc.enigma.api.Enigma; -import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; -import org.quiltmc.enigma.api.EnigmaPlugin; -import org.quiltmc.enigma.api.EnigmaPluginContext; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; -import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; -import org.quiltmc.enigma.api.service.JarIndexerService; -import org.quiltmc.enigma.api.service.NameProposalService; -import org.quiltmc.enigma.api.source.DecompilerService; -import org.quiltmc.enigma.api.source.Decompilers; -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.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 org.jetbrains.java.decompiler.util.Pair; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceInterpreter; -import org.objectweb.asm.tree.analysis.SourceValue; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public final class BuiltinPlugin implements EnigmaPlugin { - @Override - public void init(EnigmaPluginContext ctx) { - this.registerEnumNamingService(ctx); - this.registerSpecializedMethodNamingService(ctx); - this.registerDecompilerServices(ctx); - } - - private void registerEnumNamingService(EnigmaPluginContext ctx) { - final Map, String> names = new HashMap<>(); - final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); - - ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:enum_initializer_indexer")); - - ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { - @Override - public Map, EntryMapping> getProposedNames(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"; - } - }); - } - - private void registerSpecializedMethodNamingService(EnigmaPluginContext ctx) { - ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { - @Override - public Map, EntryMapping> getProposedNames(JarIndex index) { - BridgeMethodIndex bridgeMethodIndex = index.getIndex(BridgeMethodIndex.class); - Map, EntryMapping> mappings = new HashMap<>(); - - bridgeMethodIndex.getSpecializedToBridge().forEach((specialized, bridge) -> { - EntryMapping mapping = this.createMapping(bridge.getName(), TokenType.JAR_PROPOSED); - - mappings.put(specialized, mapping); - // IndexEntryResolver#resolveEntry can return the bridge method, so we can just use the name - mappings.put(bridge, mapping); - }); - - 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:specialized_method_name_proposer"; - } - }); - } - - private void registerDecompilerServices(EnigmaPluginContext ctx) { - ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER); - ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); - ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.CFR); - ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE); - } - - private static final class EnumFieldNameFindingVisitor extends ClassVisitor { - private ClassEntry clazz; - private String className; - private final Map, String> mappings; - private final Set> enumFields = new HashSet<>(); - private final List classInits = new ArrayList<>(); - - EnumFieldNameFindingVisitor(Map, String> mappings) { - super(Enigma.ASM_VERSION); - this.mappings = mappings; - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - this.className = name; - this.clazz = new ClassEntry(name); - this.enumFields.clear(); - this.classInits.clear(); - } - - @Override - public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - if ((access & Opcodes.ACC_ENUM) != 0 - && !this.enumFields.add(Pair.of(name, descriptor))) { - throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); - } - - return super.visitField(access, name, descriptor, signature, value); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - if ("".equals(name)) { - MethodNode node = new MethodNode(this.api, access, name, descriptor, signature, exceptions); - this.classInits.add(node); - return node; - } - - return super.visitMethod(access, name, descriptor, signature, exceptions); - } - - @Override - public void visitEnd() { - super.visitEnd(); - try { - this.collectResults(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private void collectResults() throws Exception { - String owner = this.className; - Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); - - for (MethodNode mn : this.classInits) { - Frame[] frames = analyzer.analyze(this.className, mn); - - InsnList instrs = mn.instructions; - for (int i = 1; i < instrs.size(); i++) { - AbstractInsnNode instr1 = instrs.get(i - 1); - AbstractInsnNode instr2 = instrs.get(i); - String s = null; - - if (instr2.getOpcode() == Opcodes.PUTSTATIC - && ((FieldInsnNode) instr2).owner.equals(owner) - && this.enumFields.contains(Pair.of(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) - && instr1.getOpcode() == Opcodes.INVOKESPECIAL - && "".equals(((MethodInsnNode) instr1).name)) { - for (int j = 0; j < frames[i - 1].getStackSize(); j++) { - SourceValue sv = frames[i - 1].getStack(j); - for (AbstractInsnNode ci : sv.insns) { - if (ci instanceof LdcInsnNode insnNode && insnNode.cst instanceof String && s == null) { - s = (String) (insnNode.cst); - } - } - } - } - - if (s != null) { - this.mappings.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); - } - - // report otherwise? - } - } - } - } -} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinMappingFormats.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinMappingFormats.java new file mode 100644 index 000000000..40a3024e8 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinMappingFormats.java @@ -0,0 +1,36 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.quiltmc.enigma.api.EnigmaPluginContext; +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; +import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsReader; +import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsWriter; +import org.quiltmc.enigma.api.translation.mapping.serde.proguard.ProguardMappingsReader; +import org.quiltmc.enigma.api.translation.mapping.serde.srg.SrgMappingsWriter; +import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Reader; +import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Writer; + +public class BuiltinMappingFormats { + public static void register(EnigmaPluginContext ctx) { + FileType.File enigmaMapping = new FileType.File("mapping", "mappings"); + + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(EnigmaMappingsReader.FILE, EnigmaMappingsWriter.FILE, enigmaMapping, "enigma:enigma_file") + ); + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(EnigmaMappingsReader.DIRECTORY, EnigmaMappingsWriter.DIRECTORY, new FileType.Directory(enigmaMapping), "enigma:enigma_directory") + ); + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(EnigmaMappingsReader.ZIP, EnigmaMappingsWriter.ZIP, new FileType.File("zip"), "enigma:enigma_zip") + ); + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(TinyV2Reader.INSTANCE, TinyV2Writer.INSTANCE, new FileType.File("tinyv2", "tiny"), "enigma:tiny_v2") + ); + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(null, SrgMappingsWriter.INSTANCE, new FileType.File("tsrg"), "enigma:srg_file") + ); + ctx.registerService(ReadWriteService.TYPE, + ctx1 -> ReadWriteService.create(ProguardMappingsReader.INSTANCE, null, new FileType.File("txt"), "enigma:proguard") + ); + } +} 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 new file mode 100644 index 000000000..24c2f9c0d --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -0,0 +1,98 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; +import org.quiltmc.enigma.api.EnigmaPlugin; +import org.quiltmc.enigma.api.EnigmaPluginContext; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.service.DecompilerService; +import org.quiltmc.enigma.api.source.Decompilers; +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 final class BuiltinPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + registerEnumNamingService(ctx); + registerSpecializedMethodNamingService(ctx); + registerDecompilerServices(ctx); + BuiltinMappingFormats.register(ctx); + } + + private static void registerEnumNamingService(EnigmaPluginContext ctx) { + final Map, String> names = new HashMap<>(); + final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); + + ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:enum_initializer_indexer")); + + ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { + @Override + public Map, EntryMapping> getProposedNames(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"; + } + }); + } + + private static void registerSpecializedMethodNamingService(EnigmaPluginContext ctx) { + ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + BridgeMethodIndex bridgeMethodIndex = index.getIndex(BridgeMethodIndex.class); + Map, EntryMapping> mappings = new HashMap<>(); + + bridgeMethodIndex.getSpecializedToBridge().forEach((specialized, bridge) -> { + EntryMapping mapping = this.createMapping(bridge.getName(), TokenType.JAR_PROPOSED); + + mappings.put(specialized, mapping); + // IndexEntryResolver#resolveEntry can return the bridge method, so we can just use the name + mappings.put(bridge, mapping); + }); + + 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:specialized_method_name_proposer"; + } + }); + } + + private static void registerDecompilerServices(EnigmaPluginContext ctx) { + ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER); + ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); + ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.CFR); + ctx.registerService(DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE); + } +} 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 new file mode 100644 index 000000000..a9e5baeba --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java @@ -0,0 +1,118 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.jetbrains.java.decompiler.util.Pair; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; +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 java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class EnumFieldNameFindingVisitor extends ClassVisitor { + private ClassEntry clazz; + private String className; + private final Map, String> mappings; + private final Set> enumFields = new HashSet<>(); + private final List classInits = new ArrayList<>(); + + EnumFieldNameFindingVisitor(Map, String> mappings) { + super(Enigma.ASM_VERSION); + this.mappings = mappings; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.className = name; + this.clazz = new ClassEntry(name); + this.enumFields.clear(); + this.classInits.clear(); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & Opcodes.ACC_ENUM) != 0 + && !this.enumFields.add(Pair.of(name, descriptor))) { + throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); + } + + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ("".equals(name)) { + MethodNode node = new MethodNode(this.api, access, name, descriptor, signature, exceptions); + this.classInits.add(node); + return node; + } + + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + super.visitEnd(); + try { + this.collectResults(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void collectResults() throws Exception { + String owner = this.className; + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + + for (MethodNode mn : this.classInits) { + Frame[] frames = analyzer.analyze(this.className, mn); + + InsnList instrs = mn.instructions; + for (int i = 1; i < instrs.size(); i++) { + AbstractInsnNode instr1 = instrs.get(i - 1); + AbstractInsnNode instr2 = instrs.get(i); + String s = null; + + if (instr2.getOpcode() == Opcodes.PUTSTATIC + && ((FieldInsnNode) instr2).owner.equals(owner) + && this.enumFields.contains(Pair.of(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) + && instr1.getOpcode() == Opcodes.INVOKESPECIAL + && "".equals(((MethodInsnNode) instr1).name)) { + for (int j = 0; j < frames[i - 1].getStackSize(); j++) { + SourceValue sv = frames[i - 1].getStack(j); + for (AbstractInsnNode ci : sv.insns) { + if (ci instanceof LdcInsnNode insnNode && insnNode.cst instanceof String && s == null) { + s = (String) (insnNode.cst); + } + } + } + } + + if (s != null) { + this.mappings.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + } + + // report otherwise? + } + } + } +} diff --git a/enigma/src/main/resources/META-INF/services/org.quiltmc.enigma.api.EnigmaPlugin b/enigma/src/main/resources/META-INF/services/org.quiltmc.enigma.api.EnigmaPlugin index 67cad0807..9df7ec919 100644 --- a/enigma/src/main/resources/META-INF/services/org.quiltmc.enigma.api.EnigmaPlugin +++ b/enigma/src/main/resources/META-INF/services/org.quiltmc.enigma.api.EnigmaPlugin @@ -1 +1 @@ -org.quiltmc.enigma.impl.analysis.BuiltinPlugin +org.quiltmc.enigma.impl.plugin.BuiltinPlugin diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index 005ca5175..f5e24706f 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json @@ -8,6 +8,7 @@ "mapping_format.tiny_file": "Tiny File", "mapping_format.srg_file": "SRG File", "mapping_format.proguard": "Proguard", + "mapping_format.all_formats": "All Formats", "type.methods": "Methods", "type.fields": "Fields", "type.parameters": "Parameters", @@ -24,9 +25,11 @@ "menu.file.mappings.save_as": "Save Mappings As...", "menu.file.mappings.close": "Close Mappings", "menu.file.mappings.drop": "Drop Invalid Mappings", + "menu.file.mappings.file_filter": "%s (%s)", "menu.file.save.non_writeable": "Format \"%s\" is not writeable!", "menu.file.save.cannot_save": "Cannot save mappings!", "menu.file.open.non_parseable": "Format \"%s\" is not parseable!", + "menu.file.open.non_parseable.unsupported_format": "Format \"%s\" is not parseable: unsupported format!", "menu.file.open.cannot_open": "Cannot open mappings!", "menu.file.reload_mappings": "Reload Mappings", "menu.file.reload_all": "Reload Jar/Mappings", diff --git a/enigma/src/test/java/org/quiltmc/enigma/DecompilationTest.java b/enigma/src/test/java/org/quiltmc/enigma/DecompilationTest.java index 225fe11fb..854eeb4fc 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/DecompilationTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/DecompilationTest.java @@ -4,7 +4,7 @@ import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Decompilers; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.junit.jupiter.params.ParameterizedTest; diff --git a/enigma/src/test/java/org/quiltmc/enigma/EnigmaProfileTest.java b/enigma/src/test/java/org/quiltmc/enigma/EnigmaProfileTest.java index fdcf408f1..8114abe5a 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/EnigmaProfileTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/EnigmaProfileTest.java @@ -6,7 +6,7 @@ import org.quiltmc.enigma.api.EnigmaProfile; import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.service.NameProposalService; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; import org.quiltmc.enigma.util.Either; diff --git a/enigma/src/test/java/org/quiltmc/enigma/MappingFormatDetectionTest.java b/enigma/src/test/java/org/quiltmc/enigma/MappingFormatDetectionTest.java index f5fdb7d38..262bc0a9d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/MappingFormatDetectionTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/MappingFormatDetectionTest.java @@ -1,22 +1,52 @@ package org.quiltmc.enigma; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import com.google.common.io.MoreFiles; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; import java.io.File; import java.util.Objects; +import java.util.Optional; public class MappingFormatDetectionTest { @Test void testFormatDetection() { File formatsDir = new File(TestUtil.getResource("/formats").toUri()); + Enigma enigma = Enigma.create(); for (File file : Objects.requireNonNull(formatsDir.listFiles())) { - MappingFormat parsedFormat = MappingFormat.parseFromFile(file.toPath()); - MappingFormat expectedFormat = MappingFormat.valueOf(file.getName().toUpperCase().split("_EXAMPLE_MAPPING")[0]); + Optional parsedFormat = enigma.getReadWriteService(file.toPath()); + if (parsedFormat.isEmpty()) { + if (file.getName().equals("invalid_mapping.garbage")) { + continue; + } - Assertions.assertSame(expectedFormat, parsedFormat, "Failed to detect format for " + file.getName()); + Assertions.fail("Failed to detect format for " + file.getName()); + } + + String expectedId; + if (file.isDirectory()) { + Assertions.assertInstanceOf(FileType.Directory.class, parsedFormat.get().getFileType(), "Failed to detect directory format for " + file.getName()); + + expectedId = switch (file.getName()) { + case "enigma_directory_example_mapping" -> "enigma:enigma_directory"; + default -> throw new IllegalStateException("Unexpected read/write service ID: " + parsedFormat.get().getId()); + }; + } else { + expectedId = switch (MoreFiles.getFileExtension(file.toPath())) { + case "tiny" -> "enigma:tiny_v2"; + case "tsrg" -> "enigma:srg_file"; + case "txt" -> "enigma:proguard"; + case "zip" -> "enigma:enigma_zip"; + case "mapping" -> "enigma:enigma_file"; + default -> throw new IllegalStateException("Unexpected read/write service ID: " + parsedFormat.get().getId()); + }; + } + + Assertions.assertEquals(expectedId, parsedFormat.get().getId()); } } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestActiveByDefault.java b/enigma/src/test/java/org/quiltmc/enigma/TestActiveByDefault.java new file mode 100644 index 000000000..b08aa9dbb --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/TestActiveByDefault.java @@ -0,0 +1,43 @@ +package org.quiltmc.enigma; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.service.DecompilerService; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.service.ReadWriteService; + +import java.io.Reader; +import java.io.StringReader; + +public class TestActiveByDefault { + @Test + public void testServicesLoaded() { + Enigma enigma = Enigma.builder().build(); + Assertions.assertFalse(enigma.getServices().get(DecompilerService.TYPE).isEmpty()); + Assertions.assertFalse(enigma.getServices().get(ReadWriteService.TYPE).isEmpty()); + Assertions.assertTrue(enigma.getServices().get(NameProposalService.TYPE).isEmpty()); + } + + @Test + public void testProfile() { + Reader r = new StringReader(""" + { + "services": { + "decompiler": { + "id": "enigma:vineflower" + } + } + }"""); + EnigmaProfile profile = EnigmaProfile.parse(r); + + // only vf should load since decompilers are explicitly defined + Enigma enigma = Enigma.builder().setProfile(profile).build(); + Assertions.assertEquals(1, enigma.getServices().get(DecompilerService.TYPE).size()); + Assertions.assertEquals("enigma:vineflower", enigma.getServices().get(DecompilerService.TYPE).get(0).getId()); + + // read write services should all be loaded + Assertions.assertFalse(enigma.getServices().get(ReadWriteService.TYPE).isEmpty()); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java index 8b4202b57..e5de80399 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java @@ -7,7 +7,7 @@ import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Decompilers; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.hamcrest.Matchers; diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestPluginValidation.java b/enigma/src/test/java/org/quiltmc/enigma/TestPluginValidation.java new file mode 100644 index 000000000..9b7bd3e83 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/TestPluginValidation.java @@ -0,0 +1,109 @@ +package org.quiltmc.enigma; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaPlugin; +import org.quiltmc.enigma.api.EnigmaPluginContext; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.MappingDelta; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; + +import java.nio.file.Path; +import java.util.List; + +public class TestPluginValidation { + @Test + public void testIdValidation() { + Enigma.builder().setPlugins(List.of(new IdTestPlugin())).build(); + } + + @Test + public void testDuplicateServices() { + Enigma.builder().setPlugins(List.of(new DuplicateIdTestPlugin())).build(); + } + + @Test + public void testDuplicateFileTypes() { + Enigma.builder().setPlugins(List.of(new DuplicateFileTypeTestPlugin())).build(); + } + + private static void registerService(EnigmaPluginContext ctx, String id) { + registerService(ctx, id, id); + } + + private static void registerService(EnigmaPluginContext ctx, String id, String fileType) { + ctx.registerService(ReadWriteService.TYPE, ctx1 -> new ReadWriteService() { + @Override + public void write(EntryTree mappings, MappingDelta delta, Path path, ProgressListener progress, MappingSaveParameters saveParameters) { + } + + @Override + public EntryTree read(Path path, ProgressListener progress) { + return null; + } + + @Override + public FileType getFileType() { + return new FileType.File(fileType); + } + + @Override + public boolean supportsReading() { + return false; + } + + @Override + public boolean supportsWriting() { + return false; + } + + @Override + public String getId() { + return id; + } + }); + } + + private static class DuplicateFileTypeTestPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + registerService(ctx, "test:grind", "gaming"); + Assertions.assertThrows(IllegalStateException.class, () -> registerService(ctx, "test:slay", "gaming")); + } + } + + private static class DuplicateIdTestPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + registerService(ctx, "grind:ground"); + Assertions.assertThrows(IllegalStateException.class, () -> registerService(ctx, "grind:ground")); + } + } + + private static class IdTestPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + // empty + Assertions.assertThrows(IllegalArgumentException.class, () -> registerService(ctx, "")); + // no namespace + Assertions.assertThrows(IllegalArgumentException.class, () -> registerService(ctx, "grind")); + // slashes in wrong place + Assertions.assertThrows(IllegalArgumentException.class, () -> registerService(ctx, "grind/grind:ground")); + // uppercase chars + Assertions.assertThrows(IllegalArgumentException.class, () -> registerService(ctx, "grind:Ground")); + // invalid chars + Assertions.assertThrows(IllegalArgumentException.class, () -> registerService(ctx, "grind:ground!")); + // valid + registerService(ctx, "grind:ground"); + registerService(ctx, "grind:ground_"); + registerService(ctx, "grind:ground_grind/g_rind2"); + registerService(ctx, "grind:ground/grind"); + registerService(ctx, "grind:ground/grind/grind"); + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestTranslator.java b/enigma/src/test/java/org/quiltmc/enigma/TestTranslator.java index 34b45384f..22a1c4c3c 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestTranslator.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestTranslator.java @@ -7,7 +7,6 @@ import org.quiltmc.enigma.api.translation.TranslateResult; import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.junit.jupiter.api.BeforeAll; @@ -30,7 +29,7 @@ public class TestTranslator { public static void beforeClass() throws Exception { enigma = Enigma.create(); project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); - mappings = MappingFormat.ENIGMA_FILE.read( + mappings = enigma.getReadWriteService(Path.of("/translation.mappings")).get().read( TestUtil.getResource("/translation.mappings"), ProgressListener.createEmpty()); project.setMappings(mappings, ProgressListener.createEmpty()); diff --git a/enigma/src/test/java/org/quiltmc/enigma/TokenChecker.java b/enigma/src/test/java/org/quiltmc/enigma/TokenChecker.java index e202d9f11..e7644288b 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TokenChecker.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TokenChecker.java @@ -5,7 +5,7 @@ import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; import org.quiltmc.enigma.api.source.Decompiler; -import org.quiltmc.enigma.api.source.DecompilerService; +import org.quiltmc.enigma.api.service.DecompilerService; import org.quiltmc.enigma.api.source.Source; import org.quiltmc.enigma.api.source.SourceIndex; import org.quiltmc.enigma.api.source.SourceSettings; diff --git a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestComments.java b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestComments.java index 81d0e183b..0dc32035d 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestComments.java +++ b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestComments.java @@ -1,12 +1,14 @@ package org.quiltmc.enigma.translation.mapping; import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.MappingDelta; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsReader; -import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Writer; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.junit.jupiter.api.Test; @@ -18,11 +20,12 @@ public class TestComments { @Test public void testParseAndWrite() throws IOException, MappingParseException { - MappingSaveParameters params = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + MappingSaveParameters params = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, "intermediary", "named"); EntryTree mappings = EnigmaMappingsReader.DIRECTORY.read( DIRECTORY); - new TinyV2Writer("intermediary", "named") - .write(mappings, DIRECTORY.resolve("convertedtiny.tiny"), params); + Path file = DIRECTORY.resolve("convertedtiny.tiny"); + + Enigma.create().getReadWriteService(file).get().write(mappings, MappingDelta.added(mappings), file, ProgressListener.createEmpty(), params); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestDeterministicWrite.java b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestDeterministicWrite.java index 4caebc463..87b1cf53c 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestDeterministicWrite.java +++ b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestDeterministicWrite.java @@ -2,9 +2,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; import org.quiltmc.enigma.api.translation.representation.ArgumentDescriptor; @@ -26,18 +28,19 @@ public class TestDeterministicWrite { @Test public void testTinyV2() throws Exception { Path dir = Files.createTempDirectory("enigmaDeterministicTinyV2-"); + Enigma enigma = Enigma.create(); EntryTree mappings = randomMappingTree(1L); String prev = null; for (int i = 0; i < 32; i++) { Path file = dir.resolve(i + ".tiny"); - MappingFormat.TINY_V2.write(mappings, file, ProgressListener.createEmpty(), null); + enigma.getReadWriteService(file).get().write(mappings, file, ProgressListener.createEmpty(), new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, null, null)); String content = Files.readString(file); if (prev != null) Assertions.assertEquals(prev, content, "Iteration " + i + " has a different result from the previous one"); prev = content; - mappings = MappingFormat.TINY_V2.read(file, ProgressListener.createEmpty()); + mappings = enigma.getReadWriteService(file).get().read(file, ProgressListener.createEmpty()); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestReadWriteCycle.java b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestReadWriteCycle.java index f2f6d25a2..edd6471ec 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestReadWriteCycle.java +++ b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestReadWriteCycle.java @@ -1,10 +1,12 @@ package org.quiltmc.enigma.translation.mapping; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.service.ReadWriteService; import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.serde.FileType; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; -import org.quiltmc.enigma.api.translation.mapping.serde.MappingFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; @@ -19,12 +21,14 @@ import java.io.File; import java.io.IOException; +import java.util.function.Predicate; /** * Tests that a MappingFormat can write out a fixed set of mappings and read them back without losing any information. */ public class TestReadWriteCycle { - private final MappingSaveParameters parameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + private final MappingSaveParameters parameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, null, null); + private final Enigma enigma = Enigma.create(); private final Pair testClazz = new Pair<>( new ClassEntry("a/b/c"), @@ -55,7 +59,7 @@ private void insertMapping(EntryTree mappings, Pair testMappings = new HashEntryTree<>(); this.insertMapping(testMappings, this.testClazz); @@ -73,10 +77,10 @@ private void testReadWriteCycle(MappingFormat mappingFormat, String tmpNameSuffi File tempFile = File.createTempFile("readWriteCycle", tmpNameSuffix); tempFile.delete(); //remove the auto created file - mappingFormat.write(testMappings, tempFile.toPath(), ProgressListener.createEmpty(), this.parameters); + readWriteService.write(testMappings, tempFile.toPath(), ProgressListener.createEmpty(), this.parameters); Assertions.assertTrue(tempFile.exists(), "Written file not created"); - EntryTree loadedMappings = mappingFormat.read(tempFile.toPath(), ProgressListener.createEmpty()); + EntryTree loadedMappings = readWriteService.read(tempFile.toPath(), ProgressListener.createEmpty()); Assertions.assertTrue(loadedMappings.contains(this.testClazz.a()), "Loaded mappings don't contain testClazz"); Assertions.assertTrue(loadedMappings.contains(this.testField1.a()), "Loaded mappings don't contain testField1"); @@ -101,21 +105,26 @@ private void testReadWriteCycle(MappingFormat mappingFormat, String tmpNameSuffi @Test public void testEnigmaFile() throws IOException, MappingParseException { - this.testReadWriteCycle(MappingFormat.ENIGMA_FILE, ".enigma"); + this.testReadWriteCycle(this.getService(file -> file.getExtensions().contains("mapping") && !file.isDirectory()), ".mapping"); } @Test public void testEnigmaDir() throws IOException, MappingParseException { - this.testReadWriteCycle(MappingFormat.ENIGMA_DIRECTORY, ".tmp"); + this.testReadWriteCycle(this.getService(file -> file.getExtensions().contains("mapping") && file.isDirectory()), ".tmp"); } @Test public void testEnigmaZip() throws IOException, MappingParseException { - this.testReadWriteCycle(MappingFormat.ENIGMA_ZIP, ".zip"); + this.testReadWriteCycle(this.getService(file -> file.getExtensions().contains("zip")), ".zip"); } @Test public void testTinyV2() throws IOException, MappingParseException { - this.testReadWriteCycle(MappingFormat.TINY_V2, ".tiny"); + this.testReadWriteCycle(this.getService(file -> file.getExtensions().contains("tiny")), ".tiny"); + } + + @SuppressWarnings("all") + private ReadWriteService getService(Predicate predicate) { + return this.enigma.getReadWriteService(this.enigma.getSupportedFileTypes().stream().filter(predicate).findFirst().get()).get(); } } diff --git a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestV2Main.java b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestV2Main.java index 39f012cc1..130db80c5 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestV2Main.java +++ b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/TestV2Main.java @@ -1,11 +1,12 @@ package org.quiltmc.enigma.translation.mapping; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.MappingDelta; import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; import org.quiltmc.enigma.api.translation.mapping.serde.enigma.EnigmaMappingsReader; -import org.quiltmc.enigma.api.translation.mapping.serde.tinyv2.TinyV2Writer; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; import java.nio.file.Path; @@ -15,10 +16,11 @@ public final class TestV2Main { public static void main(String... args) throws Exception { Path path = TestTinyV2InnerClasses.MAPPINGS; - MappingSaveParameters parameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false); + MappingSaveParameters parameters = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, "obf", "deobf"); EntryTree tree = EnigmaMappingsReader.DIRECTORY.read(path); + Path file = Paths.get("currentYarn.tiny"); - new TinyV2Writer("obf", "deobf").write(tree, Paths.get("currentYarn.tiny"), ProgressListener.createEmpty(), parameters); + Enigma.create().getReadWriteService(file).get().write(tree, MappingDelta.added(tree), file, ProgressListener.createEmpty(), parameters); } } diff --git a/enigma/src/test/resources/formats/invalid_mapping.garbage b/enigma/src/test/resources/formats/invalid_mapping.garbage new file mode 100644 index 000000000..5cde4fac1 --- /dev/null +++ b/enigma/src/test/resources/formats/invalid_mapping.garbage @@ -0,0 +1 @@ +hjgjghjkkghjggukuyyuiloyu diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e4b1d15c..bf9f11f26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ vineflower = "1.10.0" cfr = "0.2.2" procyon = "0.6.0" -proguard = "7.3.2" +proguard = "7.4.0" junit = "5.9.3" hamcrest = "2.2" jimfs = "1.2" @@ -50,6 +50,7 @@ procyon = { module = "org.bitbucket.mstrobel:procyon-compilertools", version.ref proguard = { module = "com.guardsquare:proguard-base", version.ref = "proguard" } junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit_engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit_launcher = { module = "org.junit.platform:junit-platform-launcher" } hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } jimfs = { module = "com.google.jimfs:jimfs", version.ref = "jimfs" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa..e6441136f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f..b82aa23a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d6..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f1..25da30dbd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail