From 8b7fb2eed5d56ce2107fc2004481b27c9e4ae647 Mon Sep 17 00:00:00 2001 From: FirstMegaGame4 <84094287+FirstMegaGame4@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:33:43 +0100 Subject: [PATCH] Feature: `@Requires` Annotation (#414) * Add Requires Annotation * Fix * Add PackageRequiresStrippingVisitor * Missing Symbol :/ * Blah Blah Blah Usage Conventions * Blah Blah Blah License Stuff * `value` instead of `mods` * moving it to Top API Level * Compacting Code => So Long, Stripping Data Containers... * Removing Deprecated Fields * Removing Deprecated Inner Classes * I a word --- .../java/org/quiltmc/loader/api/Requires.java | 58 +++ .../impl/launch/knot/KnotClassDelegate.java | 14 +- .../transformer/EnvironmentStrippingData.java | 232 ++---------- .../PackageEnvironmentStrippingData.java | 35 +- .../transformer/PackageStrippingData.java | 88 +++++ .../impl/transformer/QuiltTransformer.java | 22 +- .../impl/transformer/StrippingData.java | 341 ++++++++++++++++++ .../test/lambda_strip/LambdaStripTester.java | 14 +- 8 files changed, 550 insertions(+), 254 deletions(-) create mode 100644 src/main/java/org/quiltmc/loader/api/Requires.java create mode 100644 src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java create mode 100644 src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java diff --git a/src/main/java/org/quiltmc/loader/api/Requires.java b/src/main/java/org/quiltmc/loader/api/Requires.java new file mode 100644 index 000000000..c6ddeadc9 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/api/Requires.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 FabricMC + * Copyright 2022-2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Applied to declare that the annotated element requires specific mods to exist. + *

+ * When applied to mod code this will result in quilt-loader removing that element when running without the specified mods. + *

+ * When the annotated element is removed, bytecode associated with the element will not be removed. For example, if a + * field is removed, its initializer code will not, and will cause an error on execution. + *

+ * If an overriding method has this annotation and its overridden method doesn't, unexpected behavior may happen. If an + * overridden method has this annotation while the overriding method doesn't, it is safe, but the method can be used + * from the overridden class only when specified mods are found. + *

+ * When applied to an implemented interface (via {@link ElementType#TYPE_USE}) it will be removed when running without the specified mods. + * Overridden interface methods need to be annotated separately in order to be removed. + * + * @apiNote Even is this annotation is provided by Quilt Loader, some usages of it are not recommended. In example, when + * you are using code from other mods thanks to their APIs using entrypoints, you should not implement their entrypoint + * interface on the same classes that you are using for vanilla entrypoints. Instead, you should use a separate class in + * a separated package, dedicated to the mod integration that you are making. And to continue this example by a great usage + * of this annotation, you could use it in a new package-info.java file of the current package dedicated for the + * mod integration, in order to trim the complete package, which will provide an additional safety layer by avoiding + * errors when loading and / or calling classes and elements that should not when the required mod is not present. */ +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.TYPE, ElementType.TYPE_USE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PACKAGE }) +@Documented +public @interface Requires { + + /** @return required mods */ + String[] value(); + + /** @return True if lambda methods referenced by this method should also be stripped. Has no effect when used to + * annotate classes, implements declarations, or fields. */ + boolean stripLambdas() default true; +} diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java index a656c5cdf..f63748dbf 100644 --- a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java +++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java @@ -17,6 +17,8 @@ package org.quiltmc.loader.impl.launch.knot; import net.fabricmc.api.EnvType; + +import org.quiltmc.loader.impl.transformer.PackageStrippingData; import org.quiltmc.loader.impl.util.LoaderUtil; import org.objectweb.asm.ClassReader; import org.quiltmc.loader.api.ModContainer; @@ -26,7 +28,6 @@ import org.quiltmc.loader.impl.launch.common.QuiltCodeSource; import org.quiltmc.loader.impl.launch.common.QuiltLauncherBase; import org.quiltmc.loader.impl.patch.PatchLoader; -import org.quiltmc.loader.impl.transformer.PackageEnvironmentStrippingData; import org.quiltmc.loader.impl.util.FileSystemUtil; import org.quiltmc.loader.impl.util.FileUtil; import org.quiltmc.loader.impl.util.ManifestUtil; @@ -51,7 +52,6 @@ import java.security.CodeSource; import java.security.cert.Certificate; import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -322,9 +322,13 @@ boolean computeCanLoadPackage(String pkgName, boolean allowFromParent) { // No package-info class file return true; } - PackageEnvironmentStrippingData data = new PackageEnvironmentStrippingData(QuiltLoaderImpl.ASM_VERSION, envType); - new ClassReader(bytes).accept(data, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); - return !data.stripEntirePackage; + + ClassReader reader = new ClassReader(bytes); + + PackageStrippingData strippingData = new PackageStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, modCodeSourceMap); + reader.accept(strippingData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + + return !strippingData.stripEntirePackage(); } catch (IOException e) { throw new RuntimeException("Unable to load " + fileName, e); } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java index 7d939803b..32e268bc9 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java @@ -16,265 +16,79 @@ package org.quiltmc.loader.impl.transformer; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; -import org.objectweb.asm.TypeReference; -import org.quiltmc.loader.api.minecraft.ClientOnly; -import org.quiltmc.loader.api.minecraft.DedicatedServerOnly; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; import net.fabricmc.api.EnvType; -import net.fabricmc.api.Environment; -import net.fabricmc.api.EnvironmentInterface; -import net.fabricmc.api.EnvironmentInterfaces; -/** Scans a class for Environment and EnvironmentInterface annotations to figure out what needs to be stripped. */ +/** Deprecated. All stuff were moved to {@link StrippingData}. */ +@Deprecated @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public class EnvironmentStrippingData extends ClassVisitor { - // Fabric annotations - private static final String ENVIRONMENT_DESCRIPTOR = Type.getDescriptor(Environment.class); - private static final String ENVIRONMENT_INTERFACE_DESCRIPTOR = Type.getDescriptor(EnvironmentInterface.class); - private static final String ENVIRONMENT_INTERFACES_DESCRIPTOR = Type.getDescriptor(EnvironmentInterfaces.class); - - // Quilt annotations - private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class); - private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); - - private final EnvType envType; - private final String envTypeString; - - private boolean stripEntireClass = false; - private String[] interfaces; - private final Collection stripInterfaces = new HashSet<>(); - private final Collection stripFields = new HashSet<>(); - private final Collection stripMethods = new HashSet<>(); - - /** Every method contained in this will also be contained in {@link #stripMethods}. */ - private final Collection stripMethodLambdas = new HashSet<>(); - - private class FabricEnvironmentAnnotationVisitor extends AnnotationVisitor { - private final Runnable onEnvMismatch; - - private FabricEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch) { - super(api); - this.onEnvMismatch = onEnvMismatch; - } - - @Override - public void visitEnum(String name, String descriptor, String value) { - if ("value".equals(name) && !envTypeString.equals(value)) { - onEnvMismatch.run(); - } - } - } - - private class QuiltEnvironmentAnnotationVisitor extends AnnotationVisitor { - private final Runnable onEnvMismatch; - private final Runnable onEnvMismatchLambdas; - - private boolean stripLambdas = true; - - private QuiltEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch, Runnable onEnvMismatchLambda) { - super(api); - this.onEnvMismatch = onEnvMismatch; - this.onEnvMismatchLambdas = onEnvMismatchLambda; - } - - @Override - public void visit(String name, Object value) { - if ("stripLambdas".equals(name) && Boolean.FALSE.equals(value)) { - stripLambdas = false; - } - } - - @Override - public void visitEnd() { - onEnvMismatch.run(); - if (stripLambdas && onEnvMismatchLambdas != null) { - onEnvMismatchLambdas.run(); - } - } - } - - private class FabricEnvironmentInterfaceAnnotationVisitor extends AnnotationVisitor { - private boolean envMismatch; - private Type itf; - - private FabricEnvironmentInterfaceAnnotationVisitor(int api) { - super(api); - } - - @Override - public void visitEnum(String name, String descriptor, String value) { - if ("value".equals(name) && !envTypeString.equals(value)) { - envMismatch = true; - } - } - - @Override - public void visit(String name, Object value) { - if ("itf".equals(name)) { - itf = (Type) value; - } - } - - @Override - public void visitEnd() { - if (envMismatch) { - stripInterfaces.add(itf.getInternalName()); - } - } - } - - private AnnotationVisitor visitMemberAnnotation(String descriptor, boolean visible, Runnable onEnvMismatch, - Runnable onEnvMismatchLambdas) { - if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) { - return new FabricEnvironmentAnnotationVisitor(api, onEnvMismatch); - } - - if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.SERVER) { - return new QuiltEnvironmentAnnotationVisitor(api, onEnvMismatch, onEnvMismatchLambdas); - } - - if (SERVER_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.CLIENT) { - return new QuiltEnvironmentAnnotationVisitor(api, onEnvMismatch, onEnvMismatchLambdas); - } - - return null; - } +public class EnvironmentStrippingData extends StrippingData { public EnvironmentStrippingData(int api, EnvType envType) { - super(api); - this.envType = envType; - this.envTypeString = envType.name(); + super(api, envType, new ArrayList<>()); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - this.interfaces = interfaces; + super.visit(version, access, name, signature, superName, interfaces); } @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { - if (envType == EnvType.SERVER) { - stripEntireClass = true; - } - } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { - if (envType == EnvType.CLIENT) { - stripEntireClass = true; - } - } else if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) { - return new FabricEnvironmentAnnotationVisitor(api, () -> stripEntireClass = true); - } else if (ENVIRONMENT_INTERFACE_DESCRIPTOR.equals(descriptor)) { - return new FabricEnvironmentInterfaceAnnotationVisitor(api); - } else if (ENVIRONMENT_INTERFACES_DESCRIPTOR.equals(descriptor)) { - return new AnnotationVisitor(api) { - @Override - public AnnotationVisitor visitArray(String name) { - if ("value".equals(name)) { - return new AnnotationVisitor(api) { - @Override - public AnnotationVisitor visitAnnotation(String name, String descriptor) { - return new FabricEnvironmentInterfaceAnnotationVisitor(api); - } - }; - } - - return null; - } - }; - } - - return null; + return super.visitAnnotation(descriptor, visible); } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { - - TypeReference ref = new TypeReference(typeRef); - - if (ref.getSort() != TypeReference.CLASS_EXTENDS) { - return null; - } - - int interfaceIdx = ref.getSuperTypeIndex(); - - if (interfaceIdx < 0) { - // Wrongly applied to the super class - return null; - } - - final EnvType annotationEnv; - - if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { - annotationEnv = EnvType.CLIENT; - } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { - annotationEnv = EnvType.SERVER; - } else { - return null; - } - - if (annotationEnv != envType) { - stripInterfaces.add(interfaces[interfaceIdx]); - } - - return null; + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); } @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - return new FieldVisitor(api) { - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - return visitMemberAnnotation(descriptor, visible, () -> stripFields.add(name + descriptor), null); - } - }; + return super.visitField(access, name, descriptor, signature, value); } @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, - String[] exceptions) { - String methodId = name + descriptor; - return new MethodVisitor(api) { - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - return visitMemberAnnotation( - descriptor, visible, () -> stripMethods.add(methodId), () -> stripMethodLambdas.add(methodId) - ); - } - }; + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return super.visitMethod(access, name, descriptor, signature, exceptions); } + @Override public boolean stripEntireClass() { - return stripEntireClass; + return super.stripEntireClass(); } + @Override public Collection getStripInterfaces() { - return stripInterfaces; + return super.getStripInterfaces(); } + @Override public Collection getStripFields() { - return stripFields; + return super.getStripFields(); } + @Override public Collection getStripMethods() { - return stripMethods; + return super.getStripMethods(); } + @Override public Collection getStripMethodLambdas() { - return stripMethodLambdas; + return super.getStripMethodLambdas(); } + @Override public boolean isEmpty() { - return stripInterfaces.isEmpty() && stripFields.isEmpty() && stripMethods.isEmpty(); + return super.isEmpty(); } } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/PackageEnvironmentStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/PackageEnvironmentStrippingData.java index 80b41ebf6..9989eba5d 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/PackageEnvironmentStrippingData.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/PackageEnvironmentStrippingData.java @@ -16,41 +16,30 @@ package org.quiltmc.loader.impl.transformer; +import java.util.HashMap; + import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Type; -import org.quiltmc.loader.api.minecraft.ClientOnly; -import org.quiltmc.loader.api.minecraft.DedicatedServerOnly; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; import net.fabricmc.api.EnvType; +/** Deprecated. All stuff were moved to {@link PackageStrippingData}. */ +@Deprecated @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public class PackageEnvironmentStrippingData extends ClassVisitor { - - private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class); - private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); - - private final EnvType envType; - public boolean stripEntirePackage = false; +public class PackageEnvironmentStrippingData extends PackageStrippingData { public PackageEnvironmentStrippingData(int api, EnvType envType) { - super(api); - this.envType = envType; + super(api, envType, new HashMap<>()); } @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { - if (envType == EnvType.SERVER) { - stripEntirePackage = true; - } - } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { - if (envType == EnvType.CLIENT) { - stripEntirePackage = true; - } - } - return null; + return super.visitAnnotation(descriptor, visible); + } + + @Override + public boolean stripEntirePackage() { + return super.stripEntirePackage(); } } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java new file mode 100644 index 000000000..63b459cea --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022, 2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.impl.transformer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Type; +import org.quiltmc.loader.api.Requires; +import org.quiltmc.loader.api.minecraft.ClientOnly; +import org.quiltmc.loader.api.minecraft.DedicatedServerOnly; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +import net.fabricmc.api.EnvType; + +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class PackageStrippingData extends ClassVisitor { + + private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class); + private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); + private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class); + + private final EnvType envType; + private final List mods; + + private boolean stripEntirePackage = false; + + public PackageStrippingData(int api, EnvType envType, Map modCodeSourceMap) { + super(api); + this.envType = envType; + this.mods = new ArrayList<>(modCodeSourceMap.keySet()); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { + if (envType == EnvType.SERVER) { + stripEntirePackage = true; + } + } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { + if (envType == EnvType.CLIENT) { + stripEntirePackage = true; + } + } else if (REQUIRES_DESCRIPTOR.equals(descriptor)) { + return new AnnotationVisitor(api) { + @Override + public AnnotationVisitor visitArray(String name) { + if ("value".equals(name)) { + return new AnnotationVisitor(api) { + @Override + public void visit(String name, Object value) { + if (!mods.contains(String.valueOf(value))) { + stripEntirePackage = true; + } + } + }; + } + else { + return null; + } + } + }; + } + return null; + } + + public boolean stripEntirePackage() { + return this.stripEntirePackage; + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java index 1cdb1d745..ede8c5b1c 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java @@ -39,10 +39,10 @@ final class QuiltTransformer { public static byte @Nullable [] transform(boolean isDevelopment, EnvType envType, TransformCache cache, AccessWidener accessWidener, String name, ModLoadOption mod, byte[] bytes) { boolean isMinecraftClass = mod.id().equals("minecraft"); boolean transformAccess = isMinecraftClass && QuiltLauncherBase.getLauncher().getMappingConfiguration().requiresPackageAccessHack(); - boolean environmentStrip = !isMinecraftClass || isDevelopment; + boolean strip = !isMinecraftClass || isDevelopment; boolean applyAccessWidener = isMinecraftClass && accessWidener.getTargets().contains(name); - if (!transformAccess && !environmentStrip && !applyAccessWidener) { + if (!transformAccess && !strip && !applyAccessWidener) { return bytes; } @@ -51,21 +51,21 @@ final class QuiltTransformer { ClassVisitor visitor = null; int visitorCount = 0; - if (environmentStrip) { - EnvironmentStrippingData stripData = new EnvironmentStrippingData(QuiltLoaderImpl.ASM_VERSION, envType); - classReader.accept(stripData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + if (strip) { + StrippingData data = new StrippingData(QuiltLoaderImpl.ASM_VERSION, envType, cache.getMods()); + classReader.accept(data, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); - if (stripData.stripEntireClass()) { + if (data.stripEntireClass()) { cache.hideClass(name); return null; } - Collection stripMethods = stripData.getStripMethods(); + Collection stripMethods = data.getStripMethods(); boolean stripAnyLambdas = false; - if (!stripData.getStripMethodLambdas().isEmpty()) { - LambdaStripCalculator calc = new LambdaStripCalculator(QuiltLoaderImpl.ASM_VERSION, stripData.getStripMethodLambdas()); + if (!data.getStripMethodLambdas().isEmpty()) { + LambdaStripCalculator calc = new LambdaStripCalculator(QuiltLoaderImpl.ASM_VERSION, data.getStripMethodLambdas()); classReader.accept(calc, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); Collection additionalStripMethods = calc.computeAdditionalMethodsToStrip(); @@ -77,7 +77,7 @@ final class QuiltTransformer { } } - if (!stripData.isEmpty()) { + if (!data.isEmpty()) { if (stripAnyLambdas) { // ClassWriter has a (useful) optimisation that copies over the @@ -90,7 +90,7 @@ final class QuiltTransformer { classWriter = new ClassWriter(classReader, 0); } - visitor = new ClassStripper(QuiltLoaderImpl.ASM_VERSION, classWriter, stripData.getStripInterfaces(), stripData.getStripFields(), stripMethods); + visitor = new ClassStripper(QuiltLoaderImpl.ASM_VERSION, classWriter, data.getStripInterfaces(), data.getStripFields(), stripMethods); visitorCount++; } } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java new file mode 100644 index 000000000..8b62d840c --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java @@ -0,0 +1,341 @@ +/* + * Copyright 2022, 2023 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.impl.transformer; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.TypeReference; +import org.quiltmc.loader.api.Requires; +import org.quiltmc.loader.api.minecraft.ClientOnly; +import org.quiltmc.loader.api.minecraft.DedicatedServerOnly; +import org.quiltmc.loader.api.plugin.solver.ModLoadOption; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.api.EnvironmentInterface; +import net.fabricmc.api.EnvironmentInterfaces; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +/** Scans a class for Environment, EnvironmentInterface and Requires annotations to figure out what needs to be stripped. */ +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public class StrippingData extends ClassVisitor { + + // Fabric annotations + private static final String ENVIRONMENT_DESCRIPTOR = Type.getDescriptor(Environment.class); + private static final String ENVIRONMENT_INTERFACE_DESCRIPTOR = Type.getDescriptor(EnvironmentInterface.class); + private static final String ENVIRONMENT_INTERFACES_DESCRIPTOR = Type.getDescriptor(EnvironmentInterfaces.class); + + // Quilt annotations + private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class); + private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); + private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class); + + private final EnvType envType; + private final String envTypeString; + private final List mods; + + private boolean stripEntireClass = false; + private final Collection stripInterfaces = new HashSet<>(); + private final Collection stripFields = new HashSet<>(); + private final Collection stripMethods = new HashSet<>(); + + /** Every method contained in this will also be contained in {@link #stripMethods}. */ + final Collection stripMethodLambdas = new HashSet<>(); + + private String[] interfaces; + + private class FabricEnvironmentAnnotationVisitor extends AnnotationVisitor { + private final Runnable onEnvMismatch; + + private FabricEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch) { + super(api); + this.onEnvMismatch = onEnvMismatch; + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + if ("value".equals(name) && !envTypeString.equals(value)) { + onEnvMismatch.run(); + } + } + } + + private class FabricEnvironmentInterfaceAnnotationVisitor extends AnnotationVisitor { + private boolean envMismatch; + private Type itf; + + private FabricEnvironmentInterfaceAnnotationVisitor(int api) { + super(api); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + if ("value".equals(name) && !envTypeString.equals(value)) { + envMismatch = true; + } + } + + @Override + public void visit(String name, Object value) { + if ("itf".equals(name)) { + itf = (Type) value; + } + } + + @Override + public void visitEnd() { + if (envMismatch) { + stripInterfaces.add(itf.getInternalName()); + } + } + } + + private class QuiltEnvironmentAnnotationVisitor extends AnnotationVisitor { + private final Runnable onEnvMismatch; + private final Runnable onEnvMismatchLambdas; + + private boolean stripLambdas = true; + + private QuiltEnvironmentAnnotationVisitor(int api, Runnable onEnvMismatch, Runnable onEnvMismatchLambdas) { + super(api); + this.onEnvMismatch = onEnvMismatch; + this.onEnvMismatchLambdas = onEnvMismatchLambdas; + } + + @Override + public void visit(String name, Object value) { + if ("stripLambdas".equals(name) && Boolean.FALSE.equals(value)) { + stripLambdas = false; + } + } + + @Override + public void visitEnd() { + onEnvMismatch.run(); + if (stripLambdas && onEnvMismatchLambdas != null) { + onEnvMismatchLambdas.run(); + } + } + } + + private class QuiltRequiresAnnotationVisitor extends AnnotationVisitor { + private final Runnable onModsMismatch; + private final Runnable onModsMismatchLambdas; + + private boolean stripLambdas = true; + + private QuiltRequiresAnnotationVisitor(int api, Runnable onModsMismatch, Runnable onModsMismatchLambdas) { + super(api); + this.onModsMismatch = onModsMismatch; + this.onModsMismatchLambdas = onModsMismatchLambdas; + } + + @Override + public void visit(String name, Object value) { + if ("stripLambdas".equals(name) && Boolean.FALSE.equals(value)) { + stripLambdas = false; + } + } + + @Override + public AnnotationVisitor visitArray(String name) { + if ("value".equals(name)) { + return new AnnotationVisitor(api) { + + @Override + public void visit(String name, Object value) { + if (!mods.contains(String.valueOf(value))) { + onModsMismatch.run(); + + if (stripLambdas && onModsMismatchLambdas != null) { + onModsMismatchLambdas.run(); + } + } + } + }; + } + else { + return null; + } + } + } + + private AnnotationVisitor visitMemberAnnotation(String descriptor, boolean visible, Runnable onMismatch, Runnable onMismatchLambdas) { + + if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) { + return new FabricEnvironmentAnnotationVisitor(api, onMismatch); + } + + if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.SERVER) { + return new QuiltEnvironmentAnnotationVisitor(api, onMismatch, onMismatchLambdas); + } + + if (SERVER_ONLY_DESCRIPTOR.equals(descriptor) && envType == EnvType.CLIENT) { + return new QuiltEnvironmentAnnotationVisitor(api, onMismatch, onMismatchLambdas); + } + + if (REQUIRES_DESCRIPTOR.equals(descriptor)) { + return new QuiltRequiresAnnotationVisitor(api, onMismatch, onMismatchLambdas); + } + + return null; + } + + public StrippingData(int api, EnvType envType, List mods) { + super(api); + this.envType = envType; + this.envTypeString = envType.name(); + this.mods = mods.stream().map(ModLoadOption::id).collect(Collectors.toList()); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.interfaces = interfaces; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { + if (envType == EnvType.SERVER) { + stripEntireClass = true; + } + } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { + if (envType == EnvType.CLIENT) { + stripEntireClass = true; + } + } else if (REQUIRES_DESCRIPTOR.equals(descriptor)) { + return new QuiltRequiresAnnotationVisitor(api, () -> stripEntireClass = true, null); + } else if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) { + return new FabricEnvironmentAnnotationVisitor(api, () -> stripEntireClass = true); + } else if (ENVIRONMENT_INTERFACE_DESCRIPTOR.equals(descriptor)) { + return new FabricEnvironmentInterfaceAnnotationVisitor(api); + } else if (ENVIRONMENT_INTERFACES_DESCRIPTOR.equals(descriptor)) { + return new AnnotationVisitor(api) { + @Override + public AnnotationVisitor visitArray(String name) { + if ("value".equals(name)) { + return new AnnotationVisitor(api) { + @Override + public AnnotationVisitor visitAnnotation(String name, String descriptor) { + return new FabricEnvironmentInterfaceAnnotationVisitor(api); + } + }; + } + + return null; + } + }; + } + + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + + TypeReference ref = new TypeReference(typeRef); + + if (ref.getSort() != TypeReference.CLASS_EXTENDS) { + return null; + } + + int interfaceIdx = ref.getSuperTypeIndex(); + + if (interfaceIdx < 0) { + // Wrongly applied to the super class + return null; + } + + if (REQUIRES_DESCRIPTOR.equals(descriptor)) { + return new QuiltRequiresAnnotationVisitor(api, () -> stripInterfaces.add(interfaces[interfaceIdx]), null); + } + + final EnvType annotationEnv; + + if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { + annotationEnv = EnvType.CLIENT; + } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { + annotationEnv = EnvType.SERVER; + } else { + return null; + } + + if (annotationEnv != envType) { + stripInterfaces.add(interfaces[interfaceIdx]); + } + + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return new FieldVisitor(api) { + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return visitMemberAnnotation(descriptor, visible, () -> stripFields.add(name + descriptor), null); + } + }; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + String methodId = name + descriptor; + return new MethodVisitor(api) { + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return visitMemberAnnotation( + descriptor, visible, () -> stripMethods.add(methodId), () -> stripMethodLambdas.add(methodId) + ); + } + }; + } + + public boolean stripEntireClass() { + return this.stripEntireClass; + } + + public Collection getStripInterfaces() { + return this.stripInterfaces; + } + + public Collection getStripFields() { + return this.stripFields; + } + + public Collection getStripMethods() { + return this.stripMethods; + } + + public Collection getStripMethodLambdas() { + return this.stripMethodLambdas; + } + + public boolean isEmpty() { + return this.stripInterfaces.isEmpty() && this.stripFields.isEmpty() && this.stripMethods.isEmpty(); + } +} diff --git a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java index abf771c79..a9938d63e 100644 --- a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java +++ b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java @@ -20,6 +20,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -29,11 +30,12 @@ import org.objectweb.asm.util.TraceClassVisitor; import org.quiltmc.loader.impl.QuiltLoaderImpl; import org.quiltmc.loader.impl.transformer.ClassStripper; -import org.quiltmc.loader.impl.transformer.EnvironmentStrippingData; import org.quiltmc.loader.impl.transformer.LambdaStripCalculator; import net.fabricmc.api.EnvType; +import org.quiltmc.loader.impl.transformer.StrippingData; + public class LambdaStripTester { // Not a real test, since I'm not quite sure how to check for "no lambda present" @@ -63,12 +65,12 @@ public static void main(String[] args) throws IOException { | ClassReader.SKIP_FRAMES ); - EnvironmentStrippingData stripData = new EnvironmentStrippingData(Opcodes.ASM9, EnvType.SERVER); - reader.accept(stripData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + StrippingData strip = new StrippingData(Opcodes.ASM9, EnvType.SERVER, new ArrayList<>()); + reader.accept(strip, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); - Collection stripMethods = stripData.getStripMethods(); + Collection stripMethods = strip.getStripMethods(); - LambdaStripCalculator calc = new LambdaStripCalculator(Opcodes.ASM9, stripData.getStripMethodLambdas()); + LambdaStripCalculator calc = new LambdaStripCalculator(Opcodes.ASM9, strip.getStripMethodLambdas()); reader.accept(calc, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); Collection additionalStripMethods = calc.computeAdditionalMethodsToStrip(); @@ -79,7 +81,7 @@ public static void main(String[] args) throws IOException { ClassWriter classWriter = new ClassWriter(null, 0); ClassStripper visitor = new ClassStripper( - QuiltLoaderImpl.ASM_VERSION, classWriter, stripData.getStripInterfaces(), stripData.getStripFields(), + QuiltLoaderImpl.ASM_VERSION, classWriter, strip.getStripInterfaces(), strip.getStripFields(), stripMethods ); reader.accept(visitor, 0);