diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java index 1430255..229805b 100644 --- a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java @@ -59,6 +59,7 @@ import dev.skidfuscator.obfuscator.transform.impl.misc.AhegaoTransformer; import dev.skidfuscator.obfuscator.transform.impl.number.NumberTransformer; import dev.skidfuscator.obfuscator.transform.impl.pure.PureHashTransformer; +import dev.skidfuscator.obfuscator.transform.impl.remapper.mixin.MixinTransformer; import dev.skidfuscator.obfuscator.transform.impl.sdk.SdkInjectorTransformer; import dev.skidfuscator.obfuscator.transform.impl.string.StringEncryptionType; import dev.skidfuscator.obfuscator.transform.impl.string.StringTransformerV2; @@ -754,9 +755,10 @@ public List getTransformers() { //new LoopConditionTransformer(this), /* new FlatteningFlowTransformer(this),*/ - new AhegaoTransformer(this) - //new SimpleOutlinerTransformer() + new AhegaoTransformer(this), + //new SimpleOutlinerTransformer(), // + new MixinTransformer(this) )); } else { transformers.addAll(Arrays.asList( diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinConfig.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinConfig.java new file mode 100644 index 0000000..c3a2e25 --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinConfig.java @@ -0,0 +1,23 @@ +package dev.skidfuscator.obfuscator.transform.impl.remapper.mixin; + +import com.typesafe.config.Config; +import dev.skidfuscator.config.DefaultTransformerConfig; + +public class MixinConfig extends DefaultTransformerConfig { + public MixinConfig(Config config, String path) { + super(config, path); + } + + @Override + public boolean isEnabled() { + return this.getBoolean("enabled", false); + } + + public String getRefmapPath() { + return this.getString("refmap", "not_found"); + } + + public String getMixinPath() { + return this.getString("config", "not_found"); + } +} diff --git a/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinTransformer.java b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinTransformer.java new file mode 100644 index 0000000..474abcf --- /dev/null +++ b/dev.skidfuscator.obfuscator/src/main/java/dev/skidfuscator/obfuscator/transform/impl/remapper/mixin/MixinTransformer.java @@ -0,0 +1,306 @@ +package dev.skidfuscator.obfuscator.transform.impl.remapper.mixin; + +import com.google.gson.*; +import dev.skidfuscator.config.DefaultTransformerConfig; +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.event.EventPriority; +import dev.skidfuscator.obfuscator.event.annotation.Listen; +import dev.skidfuscator.obfuscator.event.impl.transform.clazz.InitClassTransformEvent; +import dev.skidfuscator.obfuscator.event.impl.transform.skid.FinalSkidTransformEvent; +import dev.skidfuscator.obfuscator.event.impl.transform.skid.InitSkidTransformEvent; +import dev.skidfuscator.obfuscator.skidasm.SkidClassNode; +import dev.skidfuscator.obfuscator.transform.AbstractTransformer; +import dev.skidfuscator.obfuscator.util.MiscUtil; +import dev.skidfuscator.obfuscator.util.misc.Pair; +import lombok.NonNull; +import org.topdank.byteengineer.commons.data.JarResource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Transforms mixin config files that contain remapped Mixin classes + *

+ * TODO: Support Mixin Plugin transformation. + * TODO: In theory, it is possible to transform methods & fields found within Mixin classes, would require a lot more work though. + * TODO: ^ ideally, it would require for the Method, Field (and Class, to remove the requirement of the transformer holding the changes) nodes to keep track of the initial & updated name + * + * @author Trol + */ +public class MixinTransformer extends AbstractTransformer { + + private final Gson gson = new GsonBuilder().create(); + + /** + * A mixin configuration file should contain the following paths in order to be valid. + */ + private final String[] mixinConfigPathsToCheck = new String[]{"mixins", "client", "server"}; + /** + * A mixin refmap file should contain the following paths in order to be valid. + */ + private final String[] refmapPathsToCheck = new String[]{"data", "mappings"}; + + /** + * Lazily loaded mixin refmap as a json object + */ + private JsonObject mixinRefmap; + + /** + * Lazily loaded mixin configuration as a json object + */ + private JsonObject mixinConfig; + + /** + * We store classes that are marked with @Mixin annotation within our own list, together with their original name. + * Then, we check if any of them have been modified by comparing with the original name with {@link SkidClassNode#getName()} + */ + private final List> mixinsToCheck = new ArrayList<>(); + + + public MixinTransformer(Skidfuscator skidfuscator) { + super(skidfuscator, "Mixin Config Transformer"); + } + + /** + * The initialization phase we validate the files, if they are invalid, then there's no need for the program to run. + */ + @Listen + void initConfigs(InitSkidTransformEvent event) { + final String refmapPath = this.getConfig().getRefmapPath(); + final String mixinConfigPath = this.getConfig().getMixinPath(); + if (!validatePath(refmapPath, "refmap") || !validatePath(mixinConfigPath, "config") + || !parseAndGetFile(refmapPath, "refmap") || !parseAndGetFile(mixinConfigPath, "config")) { + Skidfuscator.LOGGER.warn("Mixin Transformer is disabled, due to reasons above."); + return; + } + } + + /** + * Upon the initialization of the jar paths, we obtain the Mixin classes and configuration/refmap files. + */ + @Listen(EventPriority.MONITOR) + void gatherMixins(InitClassTransformEvent event) { + if (getFailed() > 0) { + return; + } + SkidClassNode classNode = event.getClassNode(); + if (classNode.isMixin()) { + mixinsToCheck.add(new Pair<>(classNode.getName(), classNode)); + } + + if (mixinConfig.size() == 0) { + Skidfuscator.LOGGER.warn("Mixin Remapper found 0 Mixin classes. Aborting mission."); + this.fail(); + } + } + + /** + * Validates the remapped count, by checking against the original list, it's either all of them, or none of them. + * Afterward we update the files. + * NOTE: Remapper needs to not remap mixins into their own each separate package, otherwise it will fail too! + */ + @Listen(EventPriority.FINALIZER) + void transformMixins(FinalSkidTransformEvent event) { + if (getFailed() > 0) { + return; + } + // Ask if StreamAPI is usable for speed or nah. + int remappedCount = mixinsToCheck.stream().filter(it -> !it.getA().equals(it.getB().getName())).toList().size(); + if (remappedCount != mixinConfig.size()) { + this.fail(); + Skidfuscator.LOGGER.warn("Mixin Remapper remapping class mismatch: [got: " + remappedCount + ", expected: " + mixinsToCheck.size() + "]. Aborting mission."); + return; + } + String firstNodeName = mixinsToCheck.get(0).getB().getName(); + String firstQualifiedPackage = firstNodeName.substring(0, firstNodeName.lastIndexOf('.')); + for (Pair stringSkidClassNodePair : mixinsToCheck.subList(1, mixinsToCheck.size())) { + String fullyQualifiedName = stringSkidClassNodePair.getB().getName(); + String qualifiedPackage = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf('.')); + if (!firstQualifiedPackage.equals(qualifiedPackage)) { + this.fail(); + Skidfuscator.LOGGER.warn("Mixin Remapper found two Mixin classes in two different directories. Aborting mission!"); + return; + } + } + if (getFailed() > 0) { + return; + } + for (Pair pair : mixinsToCheck) { + // Maybe the remapper has support for only confusing the package name? + // i.e if it was in com.test.mixins, then it would support the following: + // com.test.mixins.client.MinecraftClientMixin + // com.test.mixins.SharedConstantsMixin + String oldName = pair.getA().substring(firstQualifiedPackage.length()); + String newName = pair.getB().getName().substring(firstQualifiedPackage.length()); + if (!updateMixinConfig(oldName.replace("/", "."), newName.replace("/", "."))) { + this.fail(); + Skidfuscator.LOGGER.warn("Mixin Remapper did not find " + oldName.replace("/", ".") + " within the Mixin configuration file"); + break; + } + if (!updateRefmapConfig(pair.getA(), pair.getB().getName())) { + this.fail(); + Skidfuscator.LOGGER.warn("Mixin Remapper did not find " + pair.getA() + " within the Mixin refmap file"); + break; + } + this.success(); + } + } + + /** + * Updates the old class name to the new one within the Mixin configuration name + * + * @param oldName The initial name of the class + * @param newName The remapped name of the class + * @return status of the name being updated within the file + */ + private Boolean updateMixinConfig(String oldName, String newName) { + boolean flag = false; + // It should only appear in one path, if it does in multiple - skill issue + // Client / Server mixins are supposed to be separated, common - for both. + for (String path : mixinConfigPathsToCheck) { + JsonArray mixins = (JsonArray) this.mixinConfig.get(path); + for (int i = 0; i < mixins.size(); i++) { + String mixin = mixins.get(i).getAsString(); + if (mixin.equals(oldName)) { + mixins.set(i, new JsonPrimitive(newName)); + flag = true; + break; + } + } + } + return flag; + } + + /** + * Updates the "mappings" and "data" of oldName to newName + * TODO: Attempt to see if any specific Mixin version uses different identifications of this, but seeing as Mixin never went out of beta, this isn't the case. + * + * @param oldName The initial name of the class + * @param newName The remapped name of the class + * @return status of the name being updated within the file + */ + private Boolean updateRefmapConfig(String oldName, String newName) { + JsonObject mappings = mixinRefmap.get("mappings").getAsJsonObject(); + if (!mappings.has(oldName)) { + return false; + } + JsonObject clazzMappings = mappings.get(oldName).getAsJsonObject(); + mappings.add(newName, clazzMappings); + mappings.remove(oldName); + + JsonObject data = mixinRefmap.get("data").getAsJsonObject(); + for (Map.Entry entry : data.entrySet()) { + JsonObject clazzData = entry.getValue().getAsJsonObject(); + + if (!clazzData.has(oldName)){ + return false; + } + JsonObject mappedData = clazzData.get(oldName).getAsJsonObject(); + clazzData.add(newName, mappedData); + clazzData.remove(oldName); + break; + } + return true; + } + + /** + * Failsafe to check if the mixin config is populated + * + * @param path the given path of the provided type + * @param type the type of file it is currently checking + * @return result of it checking if it is populated and valid. + */ + private boolean validatePath(@NonNull String path, @NonNull String type) { + if (path.equals("not_found")) { + this.fail(); + // TODO: Is there a way to show an error message? This is critically important. + Skidfuscator.LOGGER.warn("Mixin " + type + " file is not set"); + return false; + } + + if (this.skidfuscator.getJarContents().getResourceContents().namedMap().containsKey(path)) { + this.fail(); + Skidfuscator.LOGGER.warn("Mixin " + type + " at " + path + " does not exist."); + return false; + } + return true; + } + + /** + * Validates the given file if it is indeed a mixin file, for specific types it is looking for, see (mixinConfigs|refMap)pathsToCheck + * + * @param path the resource path given + * @param type the type it is currently checking + * @return parsed file as a JsonObject, or empty. + */ + private Optional isValidFormatAndParse(@NonNull String path, @NonNull String type) { + JarResource jarResource = this.skidfuscator.getJarContents().getResourceContents().namedMap().get(path); + + try { + final ByteArrayInputStream bais = new ByteArrayInputStream(jarResource.getData()); + + final InputStreamReader isr = new InputStreamReader(bais); + + JsonObject jsonObject = gson.fromJson(isr, JsonObject.class); + + String[] paths = type.equals("refmap") ? refmapPathsToCheck : mixinConfigPathsToCheck; + boolean flag = true; + for (String jsonElementPath : paths) { + if (jsonObject.has(jsonElementPath)) { + flag = false; + } + } + + if (flag) { + // Maybe it should support plugins? But for the first revision, it should be fine I think. + Skidfuscator.LOGGER.warn("Provided mixin " + type + " does not have any valid paths to remap."); + return Optional.empty(); + } + + + isr.close(); + bais.close(); + + return Optional.of(jsonObject); + } catch (IOException e) { + Skidfuscator.LOGGER.warn("Failed to close the file reading of " + path); + return Optional.empty(); + } + } + + /** + * Parses the file {@link #isValidFormatAndParse} and updates the fields mixin(Refmap|Config) + * + * @param path the path to the file + * @param type the type of file being currently parsed + * @return if it succeeded in parsing or not. + */ + private boolean parseAndGetFile(String path, String type) { + Optional parsedData = isValidFormatAndParse(path, type); + + if (parsedData.isEmpty()) { + return false; + } + if (type.equals("refmap")) { + this.mixinRefmap = parsedData.get(); + } else { + this.mixinConfig = parsedData.get(); + } + return true; + } + + @Override + protected T createConfig() { + return (T) new MixinConfig(skidfuscator.getTsConfig(), MiscUtil.toCamelCase(name)); + } + + @Override + public MixinConfig getConfig() { + return (MixinConfig) super.getConfig(); + } +}