From 8b6e32e0aea961bdf6e2aeb3163e36669a90aa10 Mon Sep 17 00:00:00 2001 From: Una Thompson Date: Fri, 27 May 2022 01:06:36 -0700 Subject: [PATCH] Add access widening, misc fixes/cleanup --- build.gradle | 2 +- src/main/java/nilloader/NilLoader.java | 156 ++++++++++++++++-- src/main/java/nilloader/WidenSet.java | 17 ++ .../annotate/OriginallyPackagePrivate.java | 20 +++ .../nilloader/annotate/OriginallyPrivate.java | 19 +++ .../annotate/OriginallyProtected.java | 19 +++ 6 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 src/main/java/nilloader/WidenSet.java create mode 100644 src/main/java/nilloader/annotate/OriginallyPackagePrivate.java create mode 100644 src/main/java/nilloader/annotate/OriginallyPrivate.java create mode 100644 src/main/java/nilloader/annotate/OriginallyProtected.java diff --git a/build.gradle b/build.gradle index 693d35c..e3db19c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ targetCompatibility = 8 group = "com.unascribed" archivesBaseName = "NilLoader" -version = "1.1.7" +version = "1.2.0" tasks.withType(JavaCompile) { options.release = 8 diff --git a/src/main/java/nilloader/NilLoader.java b/src/main/java/nilloader/NilLoader.java index c01b95a..8daf41a 100644 --- a/src/main/java/nilloader/NilLoader.java +++ b/src/main/java/nilloader/NilLoader.java @@ -39,7 +39,10 @@ import org.cadixdev.lorenz.io.srg.tsrg.TSrgReader; import org.cadixdev.lorenz.model.ClassMapping; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.ClassRemapper; @@ -87,9 +90,13 @@ public EntrypointListener(String id, String className) { private static final Map classSources = new LinkedHashMap<>(); private static final Map classSourceURLs = new LinkedHashMap<>(); + private static final Map> modWidens = new HashMap<>(); private static final Map> modMappings = new HashMap<>(); private static final Map activeModMappings = new HashMap<>(); + private static WidenSet finalWidens; + private static final Set widenSubjects = new HashSet<>(); + private static ClassFileTransformer loadTracker; private static Set loadedClasses = new HashSet<>(); @@ -131,7 +138,7 @@ public static void premain(String arg, Instrumentation ins) { builtInTransformers.add(new RelaunchClassLoaderTransformer()); ins.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> { if (classBeingRedefined != null || className == null) return classfileBuffer; - return NilLoader.transform(loader, builtInTransformers, className, classfileBuffer); + return NilLoader.transform(loader, className, classfileBuffer); }); for (Runnable r : NilLogManager.initLogs) { r.run(); @@ -275,7 +282,7 @@ private static void completePremain(Instrumentation ins) { } } } - return NilLoader.transform(loader, transformers, className, classfileBuffer); + return classfileBuffer; }); fireEntrypoint("premain"); NilLoaderLog.log.debug("{} class transformer{} registered", transformers.size(), transformers.size() == 1 ? "" : "s"); @@ -283,10 +290,49 @@ private static void completePremain(Instrumentation ins) { // clean up stuff we won't be using anymore instrumentation = null; ins.removeTransformer(loadTracker); - loadedClasses = null; for (Map.Entry> en : modMappings.entrySet()) { String id = getActiveMappingId(en.getKey()); - en.setValue(Collections.singletonMap(id, en.getValue().get(id))); + MappingSet ms = en.getValue().get(id); + if (ms != null) { + en.setValue(Collections.singletonMap(id, ms)); + } else { + en.setValue(Collections.emptyMap()); + } + } + // bake the widens + finalWidens = new WidenSet(); + for (Map.Entry> en : modWidens.entrySet()) { + String id = getActiveMappingId(en.getKey()); + WidenSet val = en.getValue().get(id); + if (val != null) { + finalWidens.widenClasses.addAll(val.widenClasses); + for (Map.Entry> men : val.widenMethods.entrySet()) { + if (!finalWidens.widenMethods.containsKey(men.getKey())) { + finalWidens.widenMethods.put(men.getKey(), new HashSet<>()); + } + finalWidens.widenMethods.get(men.getKey()).addAll(men.getValue()); + } + for (Map.Entry> fen : val.widenFields.entrySet()) { + if (!finalWidens.widenFields.containsKey(fen.getKey())) { + finalWidens.widenFields.put(fen.getKey(), new HashSet<>()); + } + finalWidens.widenFields.get(fen.getKey()).addAll(fen.getValue()); + } + } + } + checkWidenLoad(finalWidens.widenClasses); + checkWidenLoad(finalWidens.widenFields.keySet()); + checkWidenLoad(finalWidens.widenMethods.keySet()); + modWidens.clear(); + loadedClasses = null; + } + + private static void checkWidenLoad(Set classes) { + widenSubjects.addAll(classes); + for (String s : classes) { + if (loadedClasses.contains(s)) { + NilLoaderLog.log.warn("Can't widen access of {} as it was loaded prematurely! Expect fireworks!", s); + } } } @@ -325,6 +371,7 @@ private static void discoverDirectory(File dir, String... extensions) { private static boolean discover(File file, boolean addToSearchPath) { List found = new ArrayList<>(); Map mappings = new HashMap<>(); + Map widens = new HashMap<>(); try (JarFile jar = new JarFile(file)) { Enumeration iter = jar.entries(); while (iter.hasMoreElements()) { @@ -355,6 +402,43 @@ private static boolean discover(File file, boolean addToSearchPath) { } } } + JsonObject widen = mobj.getObject("widen"); + if (widen != null) { + WidenSet ws = new WidenSet(); + JsonArray wclasses = widen.getArray("classes"); + if (wclasses != null) { + for (Object clazz : wclasses) { + if (clazz instanceof String) ws.widenClasses.add(clazz.toString()); + } + } + JsonArray wmethods = widen.getArray("methods"); + if (wmethods != null) { + for (Object o : wmethods) { + if (o instanceof JsonObject) { + JsonObject m = (JsonObject)o; + String owner = m.getString("owner"); + if (!ws.widenMethods.containsKey(owner)) { + ws.widenMethods.put(owner, new HashSet<>()); + } + ws.widenMethods.get(owner).add(MethodSignature.of(m.getString("sig"))); + } + } + } + JsonArray wfields = widen.getArray("fields"); + if (wfields != null) { + for (Object o : wfields) { + if (o instanceof JsonObject) { + JsonObject m = (JsonObject)o; + String owner = m.getString("owner"); + if (!ws.widenFields.containsKey(owner)) { + ws.widenFields.put(owner, new HashSet<>()); + } + ws.widenFields.get(owner).add(parseFieldSignature(m.getString("sig"))); + } + } + } + widens.put(mapping.getKey(), ws); + } mappings.put(mapping.getKey(), ms); } } @@ -375,6 +459,7 @@ private static boolean discover(File file, boolean addToSearchPath) { for (NilMetadata meta : found) { classSources.put(file, meta.id); modMappings.put(meta.id, mappings); + modWidens.put(meta.id, widens); install(meta); } return true; @@ -475,19 +560,70 @@ private static void install(NilMetadata meta) { mods.put(meta.id, meta); } - public static byte[] transform(ClassLoader loader, List transformers, String className, byte[] classBytes) { + private static int makePublic(int access) { + return (access & ~(Opcodes.ACC_PRIVATE|Opcodes.ACC_PROTECTED)) | Opcodes.ACC_PUBLIC; + } + + public static byte[] transform(ClassLoader loader, String className, byte[] classBytes) { byte[] orig = DEBUG_DUMP || DEBUG_DECOMPILE ? classBytes : null; try { boolean changed = false; + for (ClassTransformer ct : builtInTransformers) { + try { + if ((classBytes = ct.transform(className, classBytes)) != orig) { + changed = true; + } + } catch (Throwable t) { + NilLoaderLog.log.error("Failed to transform {} via built-in transformer {}", className, ct.getClass().getName(), t); + } + } for (ClassTransformer ct : transformers) { try { if ((classBytes = ct.transform(className, classBytes)) != orig) { changed = true; } } catch (Throwable t) { - NilLoaderLog.log.error("Failed to transform {} via {}", className, ct.getClass().getName(), t); + NilLoaderLog.log.error("Failed to transform {} via mod transformer {}", className, ct.getClass().getName(), t); } } + if (widenSubjects.contains(className)) { + NilLoaderLog.log.debug("Applying widening to {}", className); + Set fields = finalWidens.widenFields.getOrDefault(className, Collections.emptySet()); + Set methods = finalWidens.widenMethods.getOrDefault(className, Collections.emptySet()); + ClassReader cr = new ClassReader(classBytes); + ClassWriter cw = new ClassWriter(cr, 0); + cr.accept(new ClassVisitor(Opcodes.ASM9, cw) { + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (finalWidens.widenClasses.contains(name)) { + NilLoaderLog.log.debug("Making class {} public", name); + access = makePublic(access); + } + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (methods.contains(MethodSignature.of(name, descriptor))) { + NilLoaderLog.log.debug("Making method {}.{}{} public", className, name, descriptor); + access = makePublic(access); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (fields.contains(FieldSignature.of(name, descriptor))) { + NilLoaderLog.log.debug("Making field {}.{}:{} public", className, name, descriptor); + access = makePublic(access); + } + return super.visitField(access, name, descriptor, signature, value); + } + + }, 0); + changed = true; + classBytes = cw.toByteArray(); + } if (changed) { String dumpName = className; byte[] before = orig; @@ -516,12 +652,10 @@ public static byte[] transform(ClassLoader loader, List transf } } return classBytes; - } catch (RuntimeException t) { + } catch (Throwable t) { if (DEBUG_DUMP) writeDump(className, orig, "before", "class"); - throw t; - } catch (Error e) { - if (DEBUG_DUMP) writeDump(className, orig, "before", "class"); - throw e; + NilLoaderLog.log.error("Error while transforming {}", className, t); + return classBytes; } } diff --git a/src/main/java/nilloader/WidenSet.java b/src/main/java/nilloader/WidenSet.java new file mode 100644 index 0000000..9cd53f3 --- /dev/null +++ b/src/main/java/nilloader/WidenSet.java @@ -0,0 +1,17 @@ +package nilloader; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.cadixdev.bombe.type.signature.FieldSignature; +import org.cadixdev.bombe.type.signature.MethodSignature; + +class WidenSet { + + public final Set widenClasses = new HashSet<>(); + public final Map> widenMethods = new HashMap<>(); + public final Map> widenFields = new HashMap<>(); + +} diff --git a/src/main/java/nilloader/annotate/OriginallyPackagePrivate.java b/src/main/java/nilloader/annotate/OriginallyPackagePrivate.java new file mode 100644 index 0000000..4e1b896 --- /dev/null +++ b/src/main/java/nilloader/annotate/OriginallyPackagePrivate.java @@ -0,0 +1,20 @@ +package nilloader.annotate; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks a member or class as having originally been package-private (default visibility), but it + * was widened to {@code public} by NilGradle's remapping. The access will automatically be widened + * if it is used. + */ +@Retention(CLASS) +@Target({ TYPE, FIELD, METHOD }) +@Documented +public @interface OriginallyPackagePrivate {} diff --git a/src/main/java/nilloader/annotate/OriginallyPrivate.java b/src/main/java/nilloader/annotate/OriginallyPrivate.java new file mode 100644 index 0000000..0c6abe3 --- /dev/null +++ b/src/main/java/nilloader/annotate/OriginallyPrivate.java @@ -0,0 +1,19 @@ +package nilloader.annotate; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks a member or class as having originally been {@code private}, but it was widened to + * {@code public} by NilGradle's remapping. The access will automatically be widened if it is used. + */ +@Retention(CLASS) +@Target({ TYPE, FIELD, METHOD }) +@Documented +public @interface OriginallyPrivate {} diff --git a/src/main/java/nilloader/annotate/OriginallyProtected.java b/src/main/java/nilloader/annotate/OriginallyProtected.java new file mode 100644 index 0000000..8af3398 --- /dev/null +++ b/src/main/java/nilloader/annotate/OriginallyProtected.java @@ -0,0 +1,19 @@ +package nilloader.annotate; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks a member or class as having originally been {@code protected}, but it was widened to + * {@code public} by NilGradle's remapping. The access will automatically be widened if it is used. + */ +@Retention(CLASS) +@Target({ TYPE, FIELD, METHOD }) +@Documented +public @interface OriginallyProtected {}