libby icon indicating copy to clipboard operation
libby copied to clipboard

Remap dependencies?

Open PikaMug opened this issue 10 months ago • 7 comments

Hello, I was wondering if libby can optionally remap libraries for use with Paper after they've been downloaded?

See https://github.com/WesJD/AnvilGUI/issues/372

PikaMug avatar Apr 27 '25 02:04 PikaMug

As I know not possible

MrAxeTv avatar Apr 27 '25 08:04 MrAxeTv

I did it in my AnvilGUI addon.

Basically, Paper does expose an API to remap libraries. However, it only remaps the libraries if it has paperweight-mappings-namespace in its Manifest File.

In my implementation, I have to make a copy of the library and add the required line in its Manifest file.

You can check my implementation here: https://github.com/BetterGUI-MC/AnvilGUI/blob/0c1978df7195802628d89b5e3a32f465db583f75/src/main/java/me/hsgamer/bettergui/anvilgui/LibLoader.java

HSGamer avatar Apr 27 '25 11:04 HSGamer

Related to this issue https://github.com/PaperMC/Paper/issues/10713

Unfortunately they marked that as not-planned.

I didn't know about the paperweight-mappings-namespace, we will probably look at that too once the v2 is nearly completed.

AlessioDP avatar Apr 28 '25 14:04 AlessioDP

Thank you, @HSGamer, that works for the time being. Looking forward to v2!

PikaMug avatar Apr 29 '25 07:04 PikaMug

It's possible to do:

  1. Resolve modifier field of type Function<byte[], byte[]> in the ClassLoader returned by org.bukkit.plugin.java.LibraryLoader.LIBRARY_LOADER_FACTORY#apply(URL[], URlClassLoader)
  2. Pass the bytes of any class file through that function to obtain a remapped version.
  3. If you care about caching the result, be sure to determine if the classes need remapping. (If the server jar/build changes)

I don't think this really needs to be implemented in Libby itself. Perhaps add a rewriter(BiFunction<String, byte[], byte[]>) to LibraryBuilder to allow remapping classes for any purpose? Where the String is the class name or path, and the rest is the input/output bytes of the class.

My implementation for inspiration
@SuppressWarnings({"UnstableApiUsage", "CallToPrintStackTrace"})
public class PaperLibraryRemapper {

    private static final Logger LOGGER = Logger.getLogger(PaperLibraryRemapper.class.getName());

    public static final boolean IS_MOJANG_MAPPED = resolveIsMojangMapped();
    public static final Function<byte[], byte[]> CLASS_REWRITER = resolveClassRewriter();

    /**
     * Remaps and rewrites a jar to fix compatibility with Mojang mapped Paper servers.
     *
     * <p>This method always assumes the server is Mojang mapped
     * <br>While it is possible that a Paper server is using Reobf mappings, this is only the
     * case if someone manually (and intentionally) builds it that way.
     *
     * <p>This method will cache remapped jars until they are invalid, as determined by {@link #requiresRemapping(Path, Path)}
     *
     * @param path the path of the library to remap
     * @param librarySavePath the path of the library root folder (used to print a more friendly message to {@link Logger log})
     * @param log the logger to write to
     * @return the path of the Remapped jar
     * @throws RuntimeException if remapping fails for any reason
     */
    public static Path remap(Path path, Path librarySavePath, Logger log) {
        try {
            Path remapped = path.resolveSibling("remapped-" + path.getFileName());

            // only remap if required
            if (!requiresRemapping(path, remapped))
                return remapped;

            log.info("Remapping: " + librarySavePath.getParent().relativize(path));

            // REMAPPER could change the original, so we need to copy it.
            // It could also return a different path as well, so we need to handle that too.
            Path remappedTmp = remapped.resolveSibling("remapped-tmp-" + path.getFileName());
            Path remappedTmp2 = null;

            try (JarOutputStream outJar = new JarOutputStream(new BufferedOutputStream(Files.newOutputStream(remapped, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)))) {

                Files.copy(path, remappedTmp, StandardCopyOption.REPLACE_EXISTING);
                remappedTmp2 = LibraryLoader.REMAPPER.apply(List.of(remappedTmp)).getFirst();

                try (JarFile inJar = new JarFile(remappedTmp2.toFile())) {
                    var inEntries = inJar.entries();
                    while (inEntries.hasMoreElements()) {
                        JarEntry in = inEntries.nextElement();

                        byte[] data;
                        try (var is = inJar.getInputStream(in)) {
                            data = is.readAllBytes();
                        }

                        String inName = in.getName();
                        if (inName.startsWith("META-INF/")) {
                            if (inName.endsWith(".SF") || inName.endsWith(".DSA") || inName.endsWith(".RSA"))
                                continue; // skip signature files to avoid verification issues
                        } else if (inName.endsWith(".class") && CLASS_REWRITER != null) {
                            data = CLASS_REWRITER.apply(data); // rewrite classes using Paper's class rewriter
                        }

                        JarEntry out = new JarEntry(inName);
                        out.setComment(in.getComment());
                        out.setMethod(in.getMethod());

                        outJar.putNextEntry(out);
                        outJar.write(data);
                        outJar.closeEntry();
                    }
                }
            } catch (Throwable t) {
                Files.deleteIfExists(remapped);
            } finally {
                Files.deleteIfExists(remappedTmp);
                if (remappedTmp2 != null)
                    Files.deleteIfExists(remappedTmp2);
            }

            return remapped;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to remap library: " + path, t);
        }
    }

    /**
     * Determines if the remapped path is either out of date or isn't a file
     *
     * @param original the original non-remapped file
     * @param remapped the path to the remapped file (even if it doesn't exist)
     * @return <p>
     *     true if any of the following conditions are met:
     *     <ul>
     *         <li>remapped doesn't exist</li>
     *         <li>original file doesn't exist</li>
     *         <li>server jar location couldn't be resolved</li>
     *         <li>server jar doesn't exist</li>
     *         <li>original is newer than remapped</li>
     *         <li>server jar is newer than remapped</li>
     *         <li>error while reading the {@link BasicFileAttributes} of the above files</li>
     *     </ul>
     * </p>
     */
    public static boolean requiresRemapping(Path original, Path remapped) {
        if (!Files.isRegularFile(original) || !Files.isRegularFile(remapped))
            return true;
        if (BukkitUtil.SERVER_JAR == null || !Files.isRegularFile(BukkitUtil.SERVER_JAR))
            return true;

        try {
            BasicFileAttributes serverAttrs = Files.readAttributes(BukkitUtil.SERVER_JAR, BasicFileAttributes.class);
            BasicFileAttributes originalAttrs = Files.readAttributes(original, BasicFileAttributes.class);
            BasicFileAttributes remappedAttrs = Files.readAttributes(remapped, BasicFileAttributes.class);

            return isNewer(serverAttrs, remappedAttrs) || isNewer(originalAttrs, remappedAttrs);
        } catch (Throwable t) {}

        return true;
    }

    /**
     * Determines if {@link BasicFileAttributes a} is newer than {@link BasicFileAttributes b}
     *
     * <p>{@link BasicFileAttributes a} is considered newer if either it's modification time, or creation time (whichever is greater)
     * is greater than the modification time, or creation time (whichever is greater) of {@link BasicFileAttributes b}
     *
     * @param a the possibly newer attributes
     * @param b the possibly older attributes
     * @return true if {@link BasicFileAttributes a} is newer than {@link BasicFileAttributes b}
     */
    public static boolean isNewer(BasicFileAttributes a, BasicFileAttributes b) {
        var aCre = a.creationTime().toInstant();
        var aMod = a.lastModifiedTime().toInstant();
        var aTime = aCre.isAfter(aMod) ? aCre : aMod;

        var bCre = b.creationTime().toInstant();
        var bMod = b.lastModifiedTime().toInstant();
        var bTime = bCre.isAfter(bMod) ? bCre : bMod;

        return aTime.isAfter(bTime);
    }

    private static boolean resolveIsMojangMapped() {
        var mappingEnvClass = Reflection.clazz("io.papermc.paper.util.MappingEnvironment");
        if (mappingEnvClass == null)
            return false;
        var reobfMethod = Reflection.declaredMethod("reobf", mappingEnvClass);
        return reobfMethod == null
                || Modifier.isStatic(reobfMethod.getModifiers())
                || !reobfMethod.getReturnType().equals(boolean.class)
                || !(boolean) Reflection.invoke(reobfMethod);
    }

    private static Function<byte[], byte[]> resolveClassRewriter() {
        // steal the class rewriter from Paper's BytecodeModifyingURLClassLoader
        // this is used to replace references to java's reflection with paper's remaping reflection system
        try (URLClassLoader urlLoader = new URLClassLoader(new URL[0])) {
            ClassLoader modifierLoader = LibraryLoader.LIBRARY_LOADER_FACTORY.apply(new URL[0], urlLoader);
            Field f = Reflection.declaredField("modifier", modifierLoader.getClass());
            return Reflection.get(f, modifierLoader);
        } catch (Throwable t) {
            LOGGER.log(Level.SEVERE, "Failed to find class rewriter. Reflective nms lookups in libraries may fail", t);
            return null;
        }
    }

}

Hope that helps.

TauCu avatar Jun 17 '25 00:06 TauCu

Hello! Here you can get my class, currently used on ItemsAdder to load FastNBT. Hope it helps <3

LoneDev6 avatar Jul 01 '25 17:07 LoneDev6

Something I forgot to post here. I've made a sample extension of the BukkitLibraryManager with the remap trick: https://gist.github.com/HSGamer/82b4e72b8eaf9cac41289cba51116497

HSGamer avatar Jul 04 '25 12:07 HSGamer