From c82479dc52ff972c625f3ac4a01aaaef2e62c27c Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 28 Apr 2024 14:55:10 -0700
Subject: [PATCH] Remap plugin libraries with namespace set to spigot (#10610)

* Remap plugin libraries with namespace set to spigot

* Remap plugin libraries with namespace set to spigot
---
 .../api/Add-hook-to-remap-library-jars.patch  |  38 +++
 .../Modify-library-loader-jars-bytecode.patch | 261 ------------------
 patches/server/Plugin-remapping.patch         | 114 +++++++-
 ...ion-calls-in-plugins-using-internals.patch | 255 +++++++++++++++++
 4 files changed, 395 insertions(+), 273 deletions(-)
 create mode 100644 patches/api/Add-hook-to-remap-library-jars.patch
 delete mode 100644 patches/server/Modify-library-loader-jars-bytecode.patch

diff --git a/patches/api/Add-hook-to-remap-library-jars.patch b/patches/api/Add-hook-to-remap-library-jars.patch
new file mode 100644
index 0000000000..af92be8749
--- /dev/null
+++ b/patches/api/Add-hook-to-remap-library-jars.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
+Date: Sun, 28 Apr 2024 13:51:08 -0700
+Subject: [PATCH] Add hook to remap library jars
+
+
+diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
++++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+@@ -0,0 +0,0 @@ public class LibraryLoader
+     private final DefaultRepositorySystemSession session;
+     private final List<RemoteRepository> repositories;
+     public static java.util.function.BiFunction<URL[], ClassLoader, URLClassLoader> LIBRARY_LOADER_FACTORY; // Paper - rewrite reflection in libraries
++    public static java.util.function.Function<List<java.nio.file.Path>, List<java.nio.file.Path>> REMAPPER; // Paper - remap libraries
+ 
+     public LibraryLoader(@NotNull Logger logger)
+     {
+@@ -0,0 +0,0 @@ public class LibraryLoader
+         }
+ 
+         List<URL> jarFiles = new ArrayList<>();
++        List<java.nio.file.Path> jarPaths = new ArrayList<>(); // Paper - remap libraries
+         for ( ArtifactResult artifact : result.getArtifactResults() )
+         {
+-            File file = artifact.getArtifact().getFile();
++            // Paper start - remap libraries
++            jarPaths.add(artifact.getArtifact().getFile().toPath());
++        }
++        if (REMAPPER != null) {
++            jarPaths = REMAPPER.apply(jarPaths);
++        }
++        for (java.nio.file.Path path : jarPaths) {
++            File file = path.toFile();
++            // Paper end - remap libraries
+ 
+             URL url;
+             try
diff --git a/patches/server/Modify-library-loader-jars-bytecode.patch b/patches/server/Modify-library-loader-jars-bytecode.patch
deleted file mode 100644
index b8012ea193..0000000000
--- a/patches/server/Modify-library-loader-jars-bytecode.patch
+++ /dev/null
@@ -1,261 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
-Date: Sun, 28 Apr 2024 11:12:14 -0700
-Subject: [PATCH] Modify library loader jars bytecode
-
-
-diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.plugin.entrypoint.classloader;
-+
-+import io.papermc.paper.pluginremap.reflect.ReflectionRemapper;
-+import java.io.IOException;
-+import java.io.InputStream;
-+import java.io.UncheckedIOException;
-+import java.net.JarURLConnection;
-+import java.net.URL;
-+import java.net.URLClassLoader;
-+import java.security.CodeSigner;
-+import java.security.CodeSource;
-+import java.util.Map;
-+import java.util.concurrent.ConcurrentHashMap;
-+import java.util.function.Function;
-+import java.util.jar.Attributes;
-+import java.util.jar.Manifest;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.objectweb.asm.ClassReader;
-+import org.objectweb.asm.ClassVisitor;
-+import org.objectweb.asm.ClassWriter;
-+
-+import static java.util.Objects.requireNonNullElse;
-+
-+public final class BytecodeModifyingURLClassLoader extends URLClassLoader {
-+    static {
-+        ClassLoader.registerAsParallelCapable();
-+    }
-+
-+    private static final Object MISSING_MANIFEST = new Object();
-+
-+    private final Function<byte[], byte[]> modifier;
-+    private final Map<String, Object> manifests = new ConcurrentHashMap<>();
-+
-+    public BytecodeModifyingURLClassLoader(
-+        final URL[] urls,
-+        final ClassLoader parent,
-+        final Function<byte[], byte[]> modifier
-+    ) {
-+        super(urls, parent);
-+        this.modifier = modifier;
-+    }
-+
-+    public BytecodeModifyingURLClassLoader(
-+        final URL[] urls,
-+        final ClassLoader parent
-+    ) {
-+        this(urls, parent, bytes -> {
-+            final ClassReader classReader = new ClassReader(bytes);
-+            final ClassWriter classWriter = new ClassWriter(classReader, 0);
-+            final ClassVisitor visitor = ReflectionRemapper.visitor(classWriter);
-+            if (visitor == classWriter) {
-+                return bytes;
-+            }
-+            classReader.accept(visitor, 0);
-+            return classWriter.toByteArray();
-+        });
-+    }
-+
-+    @Override
-+    protected Class<?> findClass(final String name) throws ClassNotFoundException {
-+        final Class<?> result;
-+        final String path = name.replace('.', '/').concat(".class");
-+        final URL url = this.findResource(path);
-+        if (url != null) {
-+            try {
-+                result = this.defineClass(name, url);
-+            } catch (final IOException e) {
-+                throw new ClassNotFoundException(name, e);
-+            }
-+        } else {
-+            result = null;
-+        }
-+        if (result == null) {
-+            throw new ClassNotFoundException(name);
-+        }
-+        return result;
-+    }
-+
-+    private Class<?> defineClass(String name, URL url) throws IOException {
-+        int i = name.lastIndexOf('.');
-+        if (i != -1) {
-+            String pkgname = name.substring(0, i);
-+            // Check if package already loaded.
-+            final @Nullable Manifest man = this.manifestFor(url);
-+            if (this.getAndVerifyPackage(pkgname, man, url) == null) {
-+                try {
-+                    if (man != null) {
-+                        this.definePackage(pkgname, man, url);
-+                    } else {
-+                        this.definePackage(pkgname, null, null, null, null, null, null, null);
-+                    }
-+                } catch (IllegalArgumentException iae) {
-+                    // parallel-capable class loaders: re-verify in case of a
-+                    // race condition
-+                    if (this.getAndVerifyPackage(pkgname, man, url) == null) {
-+                        // Should never happen
-+                        throw new AssertionError("Cannot find package " +
-+                            pkgname);
-+                    }
-+                }
-+            }
-+        }
-+        final byte[] bytes;
-+        try (final InputStream is = url.openStream()) {
-+            bytes = is.readAllBytes();
-+        }
-+
-+        final byte[] modified = this.modifier.apply(bytes);
-+
-+        final CodeSource cs = new CodeSource(url, (CodeSigner[]) null);
-+        return this.defineClass(name, modified, 0, modified.length, cs);
-+    }
-+
-+    private Package getAndVerifyPackage(
-+        String pkgname,
-+        Manifest man, URL url
-+    ) {
-+        Package pkg = getDefinedPackage(pkgname);
-+        if (pkg != null) {
-+            // Package found, so check package sealing.
-+            if (pkg.isSealed()) {
-+                // Verify that code source URL is the same.
-+                if (!pkg.isSealed(url)) {
-+                    throw new SecurityException(
-+                        "sealing violation: package " + pkgname + " is sealed");
-+                }
-+            } else {
-+                // Make sure we are not attempting to seal the package
-+                // at this code source URL.
-+                if ((man != null) && this.isSealed(pkgname, man)) {
-+                    throw new SecurityException(
-+                        "sealing violation: can't seal package " + pkgname +
-+                            ": already loaded");
-+                }
-+            }
-+        }
-+        return pkg;
-+    }
-+
-+    private boolean isSealed(String name, Manifest man) {
-+        Attributes attr = man.getAttributes(name.replace('.', '/').concat("/"));
-+        String sealed = null;
-+        if (attr != null) {
-+            sealed = attr.getValue(Attributes.Name.SEALED);
-+        }
-+        if (sealed == null) {
-+            if ((attr = man.getMainAttributes()) != null) {
-+                sealed = attr.getValue(Attributes.Name.SEALED);
-+            }
-+        }
-+        return "true".equalsIgnoreCase(sealed);
-+    }
-+
-+    private @Nullable Manifest manifestFor(final URL url) throws IOException {
-+        Manifest man = null;
-+        if (url.getProtocol().equals("jar")) {
-+            try {
-+                final Object computedManifest = this.manifests.computeIfAbsent(jarName(url), $ -> {
-+                    try {
-+                        final Manifest m = ((JarURLConnection) url.openConnection()).getManifest();
-+                        return requireNonNullElse(m, MISSING_MANIFEST);
-+                    } catch (final IOException e) {
-+                        throw new UncheckedIOException(e);
-+                    }
-+                });
-+                if (computedManifest instanceof Manifest found) {
-+                    man = found;
-+                }
-+            } catch (final UncheckedIOException e) {
-+                throw e.getCause();
-+            } catch (final IllegalArgumentException e) {
-+                throw new IOException(e);
-+            }
-+        }
-+        return man;
-+    }
-+
-+    private static String jarName(final URL sourceUrl) {
-+        final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!');
-+        if (exclamationIdx != -1) {
-+            return sourceUrl.getPath().substring(0, exclamationIdx);
-+        }
-+        throw new IllegalArgumentException("Could not find jar for URL " + sourceUrl);
-+    }
-+}
-diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
-+++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
-@@ -0,0 +0,0 @@
- package io.papermc.paper.plugin.loader;
- 
- import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
-+import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
-+import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
- import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
- import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
--import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
- import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
--import org.jetbrains.annotations.NotNull;
--
- import java.io.IOException;
- import java.net.MalformedURLException;
- import java.net.URL;
-@@ -0,0 +0,0 @@ import java.util.ArrayList;
- import java.util.List;
- import java.util.jar.JarFile;
- import java.util.logging.Logger;
-+import org.jetbrains.annotations.NotNull;
- 
- public class PaperClasspathBuilder implements PluginClasspathBuilder {
- 
-@@ -0,0 +0,0 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
-         }
- 
-         try {
--            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
-+            final URLClassLoader libraryLoader = new BytecodeModifyingURLClassLoader(urls, this.getClass().getClassLoader());
-+            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), libraryLoader);
-         } catch (IOException exception) {
-             throw new RuntimeException(exception);
-         }
-diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
-+++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
-@@ -0,0 +0,0 @@
- package io.papermc.paper.plugin.provider.type.spigot;
- 
-+import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
- import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
- import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
- import org.bukkit.plugin.InvalidDescriptionException;
- import org.bukkit.plugin.PluginDescriptionFile;
-+import org.bukkit.plugin.java.LibraryLoader;
- import org.yaml.snakeyaml.error.YAMLException;
- 
- import java.io.IOException;
-@@ -0,0 +0,0 @@ import java.util.jar.JarFile;
- 
- class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
- 
-+    static {
-+        LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new;
-+    }
-+
-     @Override
-     public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
-         // Copied from SimplePluginManager#loadPlugins
diff --git a/patches/server/Plugin-remapping.patch b/patches/server/Plugin-remapping.patch
index 7e66e41726..6bb3f1573b 100644
--- a/patches/server/Plugin-remapping.patch
+++ b/patches/server/Plugin-remapping.patch
@@ -74,10 +74,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import io.papermc.paper.plugin.provider.PluginProvider;
  import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
 +import io.papermc.paper.pluginremap.PluginRemapper;
++import java.util.function.Function;
  import joptsimple.OptionSet;
  import net.minecraft.server.dedicated.DedicatedServer;
  import org.bukkit.configuration.file.YamlConfiguration;
 -import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.plugin.java.LibraryLoader;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  import org.slf4j.Logger;
@@ -93,6 +95,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.pluginRemapper = Boolean.getBoolean("paper.disable-plugin-rewriting")
 +            ? null
 +            : PluginRemapper.create(pluginDirectory);
++        LibraryLoader.REMAPPER = this.pluginRemapper == null ? Function.identity() : this.pluginRemapper::remapLibraries;
      }
  
      private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception {
@@ -104,6 +107,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          // Register the default plugin directory
          io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath());
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -0,0 +0,0 @@
+ package io.papermc.paper.plugin.loader;
+ 
++import io.papermc.paper.plugin.PluginInitializerManager;
+ import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+ import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
+ import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
+@@ -0,0 +0,0 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+         }
+ 
+         List<Path> paths = paperLibraryStore.getPaths();
++        if (PluginInitializerManager.instance().pluginRemapper != null) {
++            paths = PluginInitializerManager.instance().pluginRemapper.remapLibraries(paths);
++        }
+         URL[] urls = new URL[paths.size()];
+         for (int i = 0; i < paths.size(); i++) {
+-            Path path = paperLibraryStore.getPaths().get(i);
++            Path path = paths.get(i);
+             try {
+                 urls[i] = path.toUri().toURL();
+             } catch (MalformedURLException e) {
 diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
@@ -397,6 +425,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public static final boolean DEBUG_LOGGING = Boolean.getBoolean("Paper.PluginRemapperDebug");
 +    private static final String PAPER_REMAPPED = ".paper-remapped";
 +    private static final String UNKNOWN_ORIGIN = "unknown-origin";
++    private static final String LIBRARIES = "libraries";
 +    private static final String EXTRA_PLUGINS = "extra-plugins";
 +    private static final String REMAP_CLASSPATH = "remap-classpath";
 +    private static final String REVERSED_MAPPINGS = "mappings/reversed";
@@ -407,6 +436,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private final RemappedPluginIndex remappedPlugins;
 +    private final RemappedPluginIndex extraPlugins;
 +    private final UnknownOriginRemappedPluginIndex unknownOrigin;
++    private final UnknownOriginRemappedPluginIndex libraries;
 +    private @Nullable CompletableFuture<IMappingFile> reversedMappings;
 +
 +    public PluginRemapper(final Path pluginsDir) {
@@ -418,6 +448,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.remappedPlugins = new RemappedPluginIndex(remappedPlugins, false);
 +        this.extraPlugins = new RemappedPluginIndex(this.remappedPlugins.dir().resolve(EXTRA_PLUGINS), true);
 +        this.unknownOrigin = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(UNKNOWN_ORIGIN));
++        this.libraries = new UnknownOriginRemappedPluginIndex(this.remappedPlugins.dir().resolve(LIBRARIES));
 +    }
 +
 +    public static @Nullable PluginRemapper create(final Path pluginsDir) {
@@ -446,6 +477,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.remappedPlugins.write();
 +        this.extraPlugins.write();
 +        this.unknownOrigin.write(clean);
++        this.libraries.write(clean);
 +    }
 +
 +    // Called on startup and reload
@@ -465,6 +497,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.save(false);
 +    }
 +
++    public List<Path> remapLibraries(final List<Path> libraries) {
++        final List<CompletableFuture<Path>> tasks = new ArrayList<>();
++        for (final Path lib : libraries) {
++            if (!lib.getFileName().toString().endsWith(".jar")) {
++                if (DEBUG_LOGGING) {
++                    LOGGER.info("Library '{}' is not a jar.", libraries);
++                }
++                tasks.add(CompletableFuture.completedFuture(lib));
++                continue;
++            }
++            final @Nullable Path cached = this.libraries.getIfPresent(lib);
++            if (cached != null) {
++                if (DEBUG_LOGGING) {
++                    LOGGER.info("Library '{}' has not changed since last remap.", libraries);
++                }
++                tasks.add(CompletableFuture.completedFuture(cached));
++                continue;
++            }
++            tasks.add(this.remapLibrary(this.libraries, lib));
++        }
++        return waitForAll(tasks);
++    }
++
 +    public Path rewritePlugin(final Path plugin) {
 +        // Already remapped
 +        if (plugin.getParent().equals(this.remappedPlugins.dir())
@@ -585,6 +640,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }, executor).thenCompose(f -> f);
 +    }
 +
++    private CompletableFuture<Path> remapPlugin(
++        final RemappedPluginIndex index,
++        final Path inputFile
++    ) {
++        return this.remap(index, inputFile, false);
++    }
++
++    private CompletableFuture<Path> remapLibrary(
++        final RemappedPluginIndex index,
++        final Path inputFile
++    ) {
++        return this.remap(index, inputFile, true);
++    }
++
 +    /**
 +     * Returns the remapped file if remapping was necessary, otherwise null.
 +     *
@@ -592,7 +661,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @param inputFile input file
 +     * @return remapped file, or inputFile if no remapping was necessary
 +     */
-+    private CompletableFuture<Path> remapPlugin(final RemappedPluginIndex index, final Path inputFile) {
++    private CompletableFuture<Path> remap(
++        final RemappedPluginIndex index,
++        final Path inputFile,
++        final boolean library
++    ) {
 +        final Path destination = index.input(inputFile);
 +
 +        try (final FileSystem fs = FileSystems.newFileSystem(inputFile, new HashMap<>())) {
@@ -608,18 +681,35 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            } else {
 +                ns = null;
 +            }
-+            if (ns != null && (ns.equals(InsertManifestAttribute.MOJANG_NAMESPACE) || ns.equals(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE))) {
-+                if (DEBUG_LOGGING) {
-+                    LOGGER.info("Plugin '{}' is already Mojang mapped.", inputFile);
++            final boolean mojangMappedManifest = ns != null && (ns.equals(InsertManifestAttribute.MOJANG_NAMESPACE) || ns.equals(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE));
++            if (library) {
++                if (mojangMappedManifest) {
++                    if (DEBUG_LOGGING) {
++                        LOGGER.info("Library '{}' is already Mojang mapped.", inputFile);
++                    }
++                    index.skip(inputFile);
++                    return CompletableFuture.completedFuture(inputFile);
++                } else if (ns == null) {
++                    if (DEBUG_LOGGING) {
++                        LOGGER.info("Library '{}' does not specify a mappings namespace (not remapping).", inputFile);
++                    }
++                    index.skip(inputFile);
++                    return CompletableFuture.completedFuture(inputFile);
 +                }
-+                index.skip(inputFile);
-+                return CompletableFuture.completedFuture(inputFile);
-+            } else if (ns == null && Files.exists(fs.getPath(PluginFileType.PAPER_PLUGIN_YML))) {
-+                if (DEBUG_LOGGING) {
-+                    LOGGER.info("Plugin '{}' is a Paper plugin with no namespace specified.", inputFile);
++            } else {
++                if (mojangMappedManifest) {
++                    if (DEBUG_LOGGING) {
++                        LOGGER.info("Plugin '{}' is already Mojang mapped.", inputFile);
++                    }
++                    index.skip(inputFile);
++                    return CompletableFuture.completedFuture(inputFile);
++                } else if (ns == null && Files.exists(fs.getPath(PluginFileType.PAPER_PLUGIN_YML))) {
++                    if (DEBUG_LOGGING) {
++                        LOGGER.info("Plugin '{}' is a Paper plugin with no namespace specified.", inputFile);
++                    }
++                    index.skip(inputFile);
++                    return CompletableFuture.completedFuture(inputFile);
 +                }
-+                index.skip(inputFile);
-+                return CompletableFuture.completedFuture(inputFile);
 +            }
 +        } catch (final IOException ex) {
 +            throw new RuntimeException("Failed to open plugin jar " + inputFile, ex);
@@ -643,7 +733,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            } catch (final Exception ex) {
 +                throw new RuntimeException("Failed to remap plugin jar '" + inputFile + "'", ex);
 +            }
-+            LOGGER.info("Done remapping plugin '{}' in {}ms.", inputFile, System.currentTimeMillis() - start);
++            LOGGER.info("Done remapping {} '{}' in {}ms.", library ? "library" : "plugin", inputFile, System.currentTimeMillis() - start);
 +            return destination;
 +        }, this.threadPool);
 +    }
diff --git a/patches/server/Remap-reflection-calls-in-plugins-using-internals.patch b/patches/server/Remap-reflection-calls-in-plugins-using-internals.patch
index 8b164ce7b7..9e8da09556 100644
--- a/patches/server/Remap-reflection-calls-in-plugins-using-internals.patch
+++ b/patches/server/Remap-reflection-calls-in-plugins-using-internals.patch
@@ -43,6 +43,197 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          if (mojName == null && MOJANG_TO_OBF.containsKey(name)) {
              mojName = name;
          }
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/BytecodeModifyingURLClassLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.pluginremap.reflect.ReflectionRemapper;
++import java.io.IOException;
++import java.io.InputStream;
++import java.io.UncheckedIOException;
++import java.net.JarURLConnection;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.security.CodeSigner;
++import java.security.CodeSource;
++import java.util.Map;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.function.Function;
++import java.util.jar.Attributes;
++import java.util.jar.Manifest;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.objectweb.asm.ClassReader;
++import org.objectweb.asm.ClassVisitor;
++import org.objectweb.asm.ClassWriter;
++
++import static java.util.Objects.requireNonNullElse;
++
++public final class BytecodeModifyingURLClassLoader extends URLClassLoader {
++    static {
++        ClassLoader.registerAsParallelCapable();
++    }
++
++    private static final Object MISSING_MANIFEST = new Object();
++
++    private final Function<byte[], byte[]> modifier;
++    private final Map<String, Object> manifests = new ConcurrentHashMap<>();
++
++    public BytecodeModifyingURLClassLoader(
++        final URL[] urls,
++        final ClassLoader parent,
++        final Function<byte[], byte[]> modifier
++    ) {
++        super(urls, parent);
++        this.modifier = modifier;
++    }
++
++    public BytecodeModifyingURLClassLoader(
++        final URL[] urls,
++        final ClassLoader parent
++    ) {
++        this(urls, parent, bytes -> {
++            final ClassReader classReader = new ClassReader(bytes);
++            final ClassWriter classWriter = new ClassWriter(classReader, 0);
++            final ClassVisitor visitor = ReflectionRemapper.visitor(classWriter);
++            if (visitor == classWriter) {
++                return bytes;
++            }
++            classReader.accept(visitor, 0);
++            return classWriter.toByteArray();
++        });
++    }
++
++    @Override
++    protected Class<?> findClass(final String name) throws ClassNotFoundException {
++        final Class<?> result;
++        final String path = name.replace('.', '/').concat(".class");
++        final URL url = this.findResource(path);
++        if (url != null) {
++            try {
++                result = this.defineClass(name, url);
++            } catch (final IOException e) {
++                throw new ClassNotFoundException(name, e);
++            }
++        } else {
++            result = null;
++        }
++        if (result == null) {
++            throw new ClassNotFoundException(name);
++        }
++        return result;
++    }
++
++    private Class<?> defineClass(String name, URL url) throws IOException {
++        int i = name.lastIndexOf('.');
++        if (i != -1) {
++            String pkgname = name.substring(0, i);
++            // Check if package already loaded.
++            final @Nullable Manifest man = this.manifestFor(url);
++            if (this.getAndVerifyPackage(pkgname, man, url) == null) {
++                try {
++                    if (man != null) {
++                        this.definePackage(pkgname, man, url);
++                    } else {
++                        this.definePackage(pkgname, null, null, null, null, null, null, null);
++                    }
++                } catch (IllegalArgumentException iae) {
++                    // parallel-capable class loaders: re-verify in case of a
++                    // race condition
++                    if (this.getAndVerifyPackage(pkgname, man, url) == null) {
++                        // Should never happen
++                        throw new AssertionError("Cannot find package " +
++                            pkgname);
++                    }
++                }
++            }
++        }
++        final byte[] bytes;
++        try (final InputStream is = url.openStream()) {
++            bytes = is.readAllBytes();
++        }
++
++        final byte[] modified = this.modifier.apply(bytes);
++
++        final CodeSource cs = new CodeSource(url, (CodeSigner[]) null);
++        return this.defineClass(name, modified, 0, modified.length, cs);
++    }
++
++    private Package getAndVerifyPackage(
++        String pkgname,
++        Manifest man, URL url
++    ) {
++        Package pkg = getDefinedPackage(pkgname);
++        if (pkg != null) {
++            // Package found, so check package sealing.
++            if (pkg.isSealed()) {
++                // Verify that code source URL is the same.
++                if (!pkg.isSealed(url)) {
++                    throw new SecurityException(
++                        "sealing violation: package " + pkgname + " is sealed");
++                }
++            } else {
++                // Make sure we are not attempting to seal the package
++                // at this code source URL.
++                if ((man != null) && this.isSealed(pkgname, man)) {
++                    throw new SecurityException(
++                        "sealing violation: can't seal package " + pkgname +
++                            ": already loaded");
++                }
++            }
++        }
++        return pkg;
++    }
++
++    private boolean isSealed(String name, Manifest man) {
++        Attributes attr = man.getAttributes(name.replace('.', '/').concat("/"));
++        String sealed = null;
++        if (attr != null) {
++            sealed = attr.getValue(Attributes.Name.SEALED);
++        }
++        if (sealed == null) {
++            if ((attr = man.getMainAttributes()) != null) {
++                sealed = attr.getValue(Attributes.Name.SEALED);
++            }
++        }
++        return "true".equalsIgnoreCase(sealed);
++    }
++
++    private @Nullable Manifest manifestFor(final URL url) throws IOException {
++        Manifest man = null;
++        if (url.getProtocol().equals("jar")) {
++            try {
++                final Object computedManifest = this.manifests.computeIfAbsent(jarName(url), $ -> {
++                    try {
++                        final Manifest m = ((JarURLConnection) url.openConnection()).getManifest();
++                        return requireNonNullElse(m, MISSING_MANIFEST);
++                    } catch (final IOException e) {
++                        throw new UncheckedIOException(e);
++                    }
++                });
++                if (computedManifest instanceof Manifest found) {
++                    man = found;
++                }
++            } catch (final UncheckedIOException e) {
++                throw e.getCause();
++            } catch (final IllegalArgumentException e) {
++                throw new IOException(e);
++            }
++        }
++        return man;
++    }
++
++    private static String jarName(final URL sourceUrl) {
++        final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!');
++        if (exclamationIdx != -1) {
++            return sourceUrl.getPath().substring(0, exclamationIdx);
++        }
++        throw new IllegalArgumentException("Could not find jar for URL " + sourceUrl);
++    }
++}
 diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
@@ -66,6 +257,70 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return classWriter.toByteArray();
      }
  }
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -0,0 +0,0 @@ package io.papermc.paper.plugin.loader;
+ 
+ import io.papermc.paper.plugin.PluginInitializerManager;
+ import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+ import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
+ import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
+-import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+ import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+-import org.jetbrains.annotations.NotNull;
+-
+ import java.io.IOException;
+ import java.net.MalformedURLException;
+ import java.net.URL;
+@@ -0,0 +0,0 @@ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.jar.JarFile;
+ import java.util.logging.Logger;
++import org.jetbrains.annotations.NotNull;
+ 
+ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+ 
+@@ -0,0 +0,0 @@ public class PaperClasspathBuilder implements PluginClasspathBuilder {
+         }
+ 
+         try {
+-            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
++            final URLClassLoader libraryLoader = new BytecodeModifyingURLClassLoader(urls, this.getClass().getClassLoader());
++            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), libraryLoader);
+         } catch (IOException exception) {
+             throw new RuntimeException(exception);
+         }
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+@@ -0,0 +0,0 @@
+ package io.papermc.paper.plugin.provider.type.spigot;
+ 
++import io.papermc.paper.plugin.entrypoint.classloader.BytecodeModifyingURLClassLoader;
+ import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
+ import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
+ import org.bukkit.plugin.InvalidDescriptionException;
+ import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.LibraryLoader;
+ import org.yaml.snakeyaml.error.YAMLException;
+ 
+ import java.io.IOException;
+@@ -0,0 +0,0 @@ import java.util.jar.JarFile;
+ 
+ class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
+ 
++    static {
++        LibraryLoader.LIBRARY_LOADER_FACTORY = BytecodeModifyingURLClassLoader::new;
++    }
++
+     @Override
+     public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws InvalidDescriptionException {
+         // Copied from SimplePluginManager#loadPlugins
 diff --git a/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java b/src/main/java/io/papermc/paper/pluginremap/reflect/PaperReflection.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000