Plugin remapping

Co-authored-by: Nassim Jahnke <nassim@njahnke.dev>
This commit is contained in:
Jason Penilla
2022-10-29 15:22:32 -07:00
parent 216388dfdf
commit 13e0a1a71e
22 changed files with 1691 additions and 70 deletions

View File

@@ -0,0 +1,63 @@
package io.papermc.paper.pluginremap;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
/**
* {@link PrintWriter}-backed logger implementation for use with {@link net.neoforged.art.api.Renamer} which
* only opens the backing writer and logs messages when the {@link PluginRemapper#DEBUG_LOGGING} system property
* is set to true.
*/
@DefaultQualifier(NonNull.class)
final class DebugLogger implements Consumer<String>, AutoCloseable {
private final @Nullable PrintWriter writer;
DebugLogger(final Path logFile) {
try {
this.writer = createWriter(logFile);
} catch (final IOException ex) {
throw new RuntimeException("Failed to initialize DebugLogger for file '" + logFile + "'", ex);
}
}
@Override
public void accept(final String line) {
this.useWriter(writer -> writer.println(line));
}
@Override
public void close() {
this.useWriter(PrintWriter::close);
}
private void useWriter(final Consumer<PrintWriter> op) {
final @Nullable PrintWriter writer = this.writer;
if (writer != null) {
op.accept(writer);
}
}
Consumer<String> debug() {
return line -> this.accept("[debug]: " + line);
}
static DebugLogger forOutputFile(final Path outputFile) {
return new DebugLogger(outputFile.resolveSibling(outputFile.getFileName() + ".log"));
}
private static @Nullable PrintWriter createWriter(final Path logFile) throws IOException {
if (!PluginRemapper.DEBUG_LOGGING) {
return null;
}
if (!Files.exists(logFile.getParent())) {
Files.createDirectories(logFile.getParent());
}
return new PrintWriter(logFile.toFile());
}
}

View File

@@ -0,0 +1,69 @@
package io.papermc.paper.pluginremap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import net.neoforged.art.api.Transformer;
final class InsertManifestAttribute implements Transformer {
static final String PAPERWEIGHT_NAMESPACE_MANIFEST_KEY = "paperweight-mappings-namespace";
static final String MOJANG_NAMESPACE = "mojang";
static final String MOJANG_PLUS_YARN_NAMESPACE = "mojang+yarn";
static final String SPIGOT_NAMESPACE = "spigot";
static final Set<String> KNOWN_NAMESPACES = Set.of(MOJANG_NAMESPACE, MOJANG_PLUS_YARN_NAMESPACE, SPIGOT_NAMESPACE);
private final String mainAttributesKey;
private final String namespace;
private final boolean createIfMissing;
private volatile boolean visitedManifest = false;
static Transformer addNamespaceManifestAttribute(final String namespace) {
return new InsertManifestAttribute(PAPERWEIGHT_NAMESPACE_MANIFEST_KEY, namespace, true);
}
InsertManifestAttribute(
final String mainAttributesKey,
final String namespace,
final boolean createIfMissing
) {
this.mainAttributesKey = mainAttributesKey;
this.namespace = namespace;
this.createIfMissing = createIfMissing;
}
@Override
public ManifestEntry process(final ManifestEntry entry) {
this.visitedManifest = true;
try {
final Manifest manifest = new Manifest(new ByteArrayInputStream(entry.getData()));
manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
manifest.write(out);
return ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray());
} catch (final IOException e) {
throw new RuntimeException("Failed to modify manifest", e);
}
}
@Override
public Collection<? extends Entry> getExtras() {
if (!this.visitedManifest && this.createIfMissing) {
final Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue(this.mainAttributesKey, this.namespace);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
manifest.write(out);
} catch (final IOException e) {
throw new RuntimeException("Failed to write manifest", e);
}
return List.of(ManifestEntry.create(Entry.STABLE_TIMESTAMP, out.toByteArray()));
}
return Transformer.super.getExtras();
}
}

View File

@@ -0,0 +1,438 @@
package io.papermc.paper.pluginremap;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.mojang.logging.LogUtils;
import io.papermc.paper.plugin.provider.type.PluginFileType;
import io.papermc.paper.util.AtomicFiles;
import io.papermc.paper.util.MappingEnvironment;
import io.papermc.paper.util.concurrent.ScalingThreadPool;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import net.minecraft.DefaultUncaughtExceptionHandlerWithName;
import net.minecraft.util.ExceptionCollector;
import net.neoforged.art.api.Renamer;
import net.neoforged.art.api.SignatureStripperConfig;
import net.neoforged.art.api.Transformer;
import net.neoforged.srgutils.IMappingFile;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.slf4j.Logger;
import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute;
@DefaultQualifier(NonNull.class)
public final class PluginRemapper {
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";
private static final Logger LOGGER = LogUtils.getClassLogger();
private final ExecutorService threadPool;
private final ReobfServer reobf;
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) {
this.threadPool = createThreadPool();
final CompletableFuture<IMappingFile> mappings = CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool);
final Path remappedPlugins = pluginsDir.resolve(PAPER_REMAPPED);
this.reversedMappings = this.reversedMappingsFuture(() -> mappings, remappedPlugins, this.threadPool);
this.reobf = new ReobfServer(remappedPlugins.resolve(REMAP_CLASSPATH), mappings, this.threadPool);
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) {
if (MappingEnvironment.reobf() || !MappingEnvironment.hasMappings()) {
return null;
}
return new PluginRemapper(pluginsDir);
}
public void shutdown() {
this.threadPool.shutdown();
this.save(true);
boolean didShutdown;
try {
didShutdown = this.threadPool.awaitTermination(3L, TimeUnit.SECONDS);
} catch (final InterruptedException ex) {
didShutdown = false;
}
if (!didShutdown) {
this.threadPool.shutdownNow();
}
}
public void save(final boolean clean) {
this.remappedPlugins.write();
this.extraPlugins.write();
this.unknownOrigin.write(clean);
this.libraries.write(clean);
}
// Called on startup and reload
public void loadingPlugins() {
if (this.reversedMappings == null) {
this.reversedMappings = this.reversedMappingsFuture(
() -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, this.threadPool),
this.remappedPlugins.dir(),
this.threadPool
);
}
}
// Called after all plugins enabled during startup/reload
public void pluginsEnabled() {
this.reversedMappings = null;
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.", lib);
}
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.", lib);
}
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())
|| plugin.getParent().equals(this.extraPlugins.dir())) {
return plugin;
}
final @Nullable Path cached = this.unknownOrigin.getIfPresent(plugin);
if (cached != null) {
if (DEBUG_LOGGING) {
LOGGER.info("Plugin '{}' has not changed since last remap.", plugin);
}
return cached;
}
return this.remapPlugin(this.unknownOrigin, plugin).join();
}
public List<Path> rewriteExtraPlugins(final List<Path> plugins) {
final @Nullable List<Path> allCached = this.extraPlugins.getAllIfPresent(plugins);
if (allCached != null) {
if (DEBUG_LOGGING) {
LOGGER.info("All extra plugins have a remapped variant cached.");
}
return allCached;
}
final List<CompletableFuture<Path>> tasks = new ArrayList<>();
for (final Path file : plugins) {
final @Nullable Path cached = this.extraPlugins.getIfPresent(file);
if (cached != null) {
if (DEBUG_LOGGING) {
LOGGER.info("Extra plugin '{}' has not changed since last remap.", file);
}
tasks.add(CompletableFuture.completedFuture(cached));
continue;
}
tasks.add(this.remapPlugin(this.extraPlugins, file));
}
return waitForAll(tasks);
}
public List<Path> rewritePluginDirectory(final List<Path> jars) {
final @Nullable List<Path> remappedJars = this.remappedPlugins.getAllIfPresent(jars);
if (remappedJars != null) {
if (DEBUG_LOGGING) {
LOGGER.info("All plugins have a remapped variant cached.");
}
return remappedJars;
}
final List<CompletableFuture<Path>> tasks = new ArrayList<>();
for (final Path file : jars) {
final @Nullable Path existingFile = this.remappedPlugins.getIfPresent(file);
if (existingFile != null) {
if (DEBUG_LOGGING) {
LOGGER.info("Plugin '{}' has not changed since last remap.", file);
}
tasks.add(CompletableFuture.completedFuture(existingFile));
continue;
}
tasks.add(this.remapPlugin(this.remappedPlugins, file));
}
return waitForAll(tasks);
}
private static IMappingFile reverse(final IMappingFile mappings) {
if (DEBUG_LOGGING) {
LOGGER.info("Reversing mappings...");
}
final long start = System.currentTimeMillis();
final IMappingFile reversed = mappings.reverse();
if (DEBUG_LOGGING) {
LOGGER.info("Done reversing mappings in {}ms.", System.currentTimeMillis() - start);
}
return reversed;
}
private CompletableFuture<IMappingFile> reversedMappingsFuture(
final Supplier<CompletableFuture<IMappingFile>> mappingsFuture,
final Path remappedPlugins,
final Executor executor
) {
return CompletableFuture.supplyAsync(() -> {
try {
final String mappingsHash = MappingEnvironment.mappingsHash();
final String fName = mappingsHash + ".tiny";
final Path reversedMappings1 = remappedPlugins.resolve(REVERSED_MAPPINGS);
final Path file = reversedMappings1.resolve(fName);
if (Files.isDirectory(reversedMappings1)) {
if (Files.isRegularFile(file)) {
return CompletableFuture.completedFuture(
loadMappings("Reversed", Files.newInputStream(file))
);
} else {
for (final Path oldFile : list(reversedMappings1, Files::isRegularFile)) {
Files.delete(oldFile);
}
}
} else {
Files.createDirectories(reversedMappings1);
}
return mappingsFuture.get().thenApply(loadedMappings -> {
final IMappingFile reversed = reverse(loadedMappings);
try {
AtomicFiles.atomicWrite(file, writeTo -> {
reversed.write(writeTo, IMappingFile.Format.TINY, false);
});
} catch (final IOException e) {
throw new RuntimeException("Failed to write reversed mappings", e);
}
return reversed;
});
} catch (final IOException e) {
throw new RuntimeException("Failed to load reversed mappings", e);
}
}, 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.
*
* @param index remapped plugin index
* @param inputFile input file
* @return remapped file, or inputFile if no remapping was necessary
*/
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<>())) {
// Leave dummy files if no remapping is required, so that we can check if they exist without copying the whole file
final Path manifestPath = fs.getPath("META-INF/MANIFEST.MF");
final @Nullable String ns;
if (Files.exists(manifestPath)) {
final Manifest manifest;
try (final InputStream in = new BufferedInputStream(Files.newInputStream(manifestPath))) {
manifest = new Manifest(in);
}
ns = manifest.getMainAttributes().getValue(InsertManifestAttribute.PAPERWEIGHT_NAMESPACE_MANIFEST_KEY);
} else {
ns = null;
}
if (ns != null && !InsertManifestAttribute.KNOWN_NAMESPACES.contains(ns)) {
throw new RuntimeException("Failed to remap plugin " + inputFile + " with unknown mapping namespace '" + ns + "'");
}
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);
}
} 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);
}
}
} catch (final IOException ex) {
return CompletableFuture.failedFuture(new RuntimeException("Failed to open plugin jar " + inputFile, ex));
}
return this.reobf.remapped().thenApplyAsync(reobfServer -> {
LOGGER.info("Remapping {} '{}'...", library ? "library" : "plugin", inputFile);
final long start = System.currentTimeMillis();
try (final DebugLogger logger = DebugLogger.forOutputFile(destination)) {
try (final Renamer renamer = Renamer.builder()
.add(Transformer.renamerFactory(this.mappings(), false))
.add(addNamespaceManifestAttribute(InsertManifestAttribute.MOJANG_PLUS_YARN_NAMESPACE))
.add(Transformer.signatureStripperFactory(SignatureStripperConfig.ALL))
.lib(reobfServer.toFile())
.threads(1)
.logger(logger)
.debug(logger.debug())
.build()) {
renamer.run(inputFile.toFile(), destination.toFile());
}
} catch (final Exception ex) {
throw new RuntimeException("Failed to remap plugin jar '" + inputFile + "'", ex);
}
LOGGER.info("Done remapping {} '{}' in {}ms.", library ? "library" : "plugin", inputFile, System.currentTimeMillis() - start);
return destination;
}, this.threadPool);
}
private IMappingFile mappings() {
final @Nullable CompletableFuture<IMappingFile> mappings = this.reversedMappings;
if (mappings == null) {
return this.reversedMappingsFuture(
() -> CompletableFuture.supplyAsync(PluginRemapper::loadReobfMappings, Runnable::run),
this.remappedPlugins.dir(),
Runnable::run
).join();
}
return mappings.join();
}
private static IMappingFile loadReobfMappings() {
return loadMappings("Reobf", MappingEnvironment.mappingsStream());
}
private static IMappingFile loadMappings(final String name, final InputStream stream) {
try (stream) {
if (DEBUG_LOGGING) {
LOGGER.info("Loading {} mappings...", name);
}
final long start = System.currentTimeMillis();
final IMappingFile load = IMappingFile.load(stream);
if (DEBUG_LOGGING) {
LOGGER.info("Done loading {} mappings in {}ms.", name, System.currentTimeMillis() - start);
}
return load;
} catch (final IOException ex) {
throw new RuntimeException("Failed to load " + name + " mappings", ex);
}
}
static List<Path> list(final Path dir, final Predicate<Path> filter) {
try (final Stream<Path> stream = Files.list(dir)) {
return stream.filter(filter).toList();
} catch (final IOException ex) {
throw new RuntimeException("Failed to list directory '" + dir + "'", ex);
}
}
private static List<Path> waitForAll(final List<CompletableFuture<Path>> tasks) {
final ExceptionCollector<Exception> collector = new ExceptionCollector<>();
final List<Path> ret = new ArrayList<>();
for (final CompletableFuture<Path> task : tasks) {
try {
ret.add(task.join());
} catch (final CompletionException ex) {
collector.add(ex);
}
}
try {
collector.throwIfPresent();
} catch (final Exception ex) {
// Don't hard fail during bootstrap/plugin loading. The plugin(s) in question will be skipped
LOGGER.error("Encountered exception remapping plugins", ex);
}
return ret;
}
private static ThreadPoolExecutor createThreadPool() {
return new ThreadPoolExecutor(
0,
4,
5L,
TimeUnit.SECONDS,
ScalingThreadPool.createUnboundedQueue(),
new ThreadFactoryBuilder()
.setNameFormat("Paper Plugin Remapper Thread - %1$d")
.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(LOGGER))
.build(),
ScalingThreadPool.defaultReEnqueuePolicy()
);
}
}

View File

@@ -0,0 +1,212 @@
package io.papermc.paper.pluginremap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.logging.LogUtils;
import io.papermc.paper.util.Hashing;
import io.papermc.paper.util.MappingEnvironment;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.slf4j.Logger;
import org.spongepowered.configurate.loader.AtomicFiles;
@DefaultQualifier(NonNull.class)
class RemappedPluginIndex {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final String INDEX_FILE_NAME = "index.json";
protected final State state;
private final Path dir;
private final Path indexFile;
private final boolean handleDuplicateFileNames;
// todo maybe hash remapped variants to ensure they haven't changed? probably unneeded
static final class State {
final Map<String, String> hashes = new HashMap<>();
final Set<String> skippedHashes = new HashSet<>();
private final String mappingsHash = MappingEnvironment.mappingsHash();
}
RemappedPluginIndex(final Path dir, final boolean handleDuplicateFileNames) {
this.dir = dir;
this.handleDuplicateFileNames = handleDuplicateFileNames;
if (!Files.exists(this.dir)) {
try {
Files.createDirectories(this.dir);
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
this.indexFile = dir.resolve(INDEX_FILE_NAME);
if (Files.isRegularFile(this.indexFile)) {
try {
this.state = this.readIndex();
} catch (final IOException e) {
throw new RuntimeException(e);
}
} else {
this.state = new State();
}
}
private State readIndex() throws IOException {
final State state;
try (final BufferedReader reader = Files.newBufferedReader(this.indexFile)) {
state = GSON.fromJson(reader, State.class);
}
// If mappings have changed, delete all cached files and create a new index
if (!state.mappingsHash.equals(MappingEnvironment.mappingsHash())) {
for (final String fileName : state.hashes.values()) {
Files.deleteIfExists(this.dir.resolve(fileName));
}
return new State();
}
return state;
}
Path dir() {
return this.dir;
}
/**
* Returns a list of cached paths if all of the input paths are present in the cache.
* The returned list may contain paths from different directories.
*
* @param paths plugin jar paths to check
* @return null if any of the paths are not present in the cache, otherwise a list of the cached paths
*/
@Nullable List<Path> getAllIfPresent(final List<Path> paths) {
final Map<Path, String> hashCache = new HashMap<>();
final Function<Path, String> inputFileHash = path -> hashCache.computeIfAbsent(path, Hashing::sha256);
// Delete cached entries we no longer need
final Iterator<Map.Entry<String, String>> iterator = this.state.hashes.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String, String> entry = iterator.next();
final String inputHash = entry.getKey();
final String fileName = entry.getValue();
if (paths.stream().anyMatch(path -> inputFileHash.apply(path).equals(inputHash))) {
// Hash is used, keep it
continue;
}
iterator.remove();
try {
Files.deleteIfExists(this.dir.resolve(fileName));
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
// Also clear hashes of skipped files
this.state.skippedHashes.removeIf(hash -> paths.stream().noneMatch(path -> inputFileHash.apply(path).equals(hash)));
final List<Path> ret = new ArrayList<>();
for (final Path path : paths) {
final String inputHash = inputFileHash.apply(path);
if (this.state.skippedHashes.contains(inputHash)) {
// Add the original path
ret.add(path);
continue;
}
final @Nullable Path cached = this.getIfPresent(inputHash);
if (cached == null) {
// Missing the remapped file
return null;
}
ret.add(cached);
}
return ret;
}
private String createCachedFileName(final Path in) {
if (this.handleDuplicateFileNames) {
final String fileName = in.getFileName().toString();
final int i = fileName.lastIndexOf(".jar");
return fileName.substring(0, i) + "-" + System.currentTimeMillis() + ".jar";
}
return in.getFileName().toString();
}
/**
* Returns the given path if the file was previously skipped for being remapped, otherwise the cached path or null.
*
* @param in input file
* @return {@code in} if already remapped, the cached path if present, otherwise null
*/
@Nullable Path getIfPresent(final Path in) {
final String inHash = Hashing.sha256(in);
if (this.state.skippedHashes.contains(inHash)) {
return in;
}
return this.getIfPresent(inHash);
}
/**
* Returns the cached path if a remapped file is present for the given hash, otherwise null.
*
* @param inHash hash of the input file
* @return the cached path if present, otherwise null
* @see #getIfPresent(Path)
*/
protected @Nullable Path getIfPresent(final String inHash) {
final @Nullable String fileName = this.state.hashes.get(inHash);
if (fileName == null) {
return null;
}
final Path path = this.dir.resolve(fileName);
if (Files.exists(path)) {
return path;
}
return null;
}
Path input(final Path in) {
return this.input(in, Hashing.sha256(in));
}
/**
* Marks the given file as skipped for remapping.
*
* @param in input file
*/
void skip(final Path in) {
this.state.skippedHashes.add(Hashing.sha256(in));
}
protected Path input(final Path in, final String hashString) {
final String name = this.createCachedFileName(in);
this.state.hashes.put(hashString, name);
return this.dir.resolve(name);
}
void write() {
try (final BufferedWriter writer = AtomicFiles.atomicBufferedWriter(this.indexFile, StandardCharsets.UTF_8)) {
GSON.toJson(this.state, writer);
} catch (final IOException ex) {
LOGGER.warn("Failed to write index file '{}'", this.indexFile, ex);
}
}
}

View File

@@ -0,0 +1,92 @@
package io.papermc.paper.pluginremap;
import com.mojang.logging.LogUtils;
import io.papermc.paper.util.AtomicFiles;
import io.papermc.paper.util.MappingEnvironment;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import net.neoforged.art.api.Renamer;
import net.neoforged.art.api.Transformer;
import net.neoforged.art.internal.RenamerImpl;
import net.neoforged.srgutils.IMappingFile;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.slf4j.Logger;
import static io.papermc.paper.pluginremap.InsertManifestAttribute.addNamespaceManifestAttribute;
@DefaultQualifier(NonNull.class)
final class ReobfServer {
private static final Logger LOGGER = LogUtils.getClassLogger();
private final Path remapClasspathDir;
private final CompletableFuture<Void> load;
ReobfServer(final Path remapClasspathDir, final CompletableFuture<IMappingFile> mappings, final Executor executor) {
this.remapClasspathDir = remapClasspathDir;
if (this.mappingsChanged()) {
this.load = mappings.thenAcceptAsync(this::remap, executor);
} else {
if (PluginRemapper.DEBUG_LOGGING) {
LOGGER.info("Have cached reobf server for current mappings.");
}
this.load = CompletableFuture.completedFuture(null);
}
}
CompletableFuture<Path> remapped() {
return this.load.thenApply($ -> this.remappedPath());
}
private Path remappedPath() {
return this.remapClasspathDir.resolve(MappingEnvironment.mappingsHash() + ".jar");
}
private boolean mappingsChanged() {
return !Files.exists(this.remappedPath());
}
private void remap(final IMappingFile mappings) {
try {
if (!Files.exists(this.remapClasspathDir)) {
Files.createDirectories(this.remapClasspathDir);
}
for (final Path file : PluginRemapper.list(this.remapClasspathDir, Files::isRegularFile)) {
Files.delete(file);
}
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
LOGGER.info("Remapping server...");
final long startRemap = System.currentTimeMillis();
try (final DebugLogger log = DebugLogger.forOutputFile(this.remappedPath())) {
AtomicFiles.atomicWrite(this.remappedPath(), writeTo -> {
try (final RenamerImpl renamer = (RenamerImpl) Renamer.builder()
.logger(log)
.debug(log.debug())
.threads(1)
.add(Transformer.renamerFactory(mappings, false))
.add(addNamespaceManifestAttribute(InsertManifestAttribute.SPIGOT_NAMESPACE))
.build()) {
renamer.run(serverJar().toFile(), writeTo.toFile(), true);
}
});
} catch (final Exception ex) {
throw new RuntimeException("Failed to remap server jar", ex);
}
LOGGER.info("Done remapping server in {}ms.", System.currentTimeMillis() - startRemap);
}
private static Path serverJar() {
try {
return Path.of(ReobfServer.class.getProtectionDomain().getCodeSource().getLocation().toURI());
} catch (final URISyntaxException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -0,0 +1,72 @@
package io.papermc.paper.pluginremap;
import com.mojang.logging.LogUtils;
import io.papermc.paper.util.Hashing;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.slf4j.Logger;
@DefaultQualifier(NonNull.class)
final class UnknownOriginRemappedPluginIndex extends RemappedPluginIndex {
private static final Logger LOGGER = LogUtils.getLogger();
private final Set<String> used = new HashSet<>();
UnknownOriginRemappedPluginIndex(final Path dir) {
super(dir, true);
}
@Override
@Nullable Path getIfPresent(final Path in) {
final String hash = Hashing.sha256(in);
if (this.state.skippedHashes.contains(hash)) {
return in;
}
final @Nullable Path path = super.getIfPresent(hash);
if (path != null) {
this.used.add(hash);
}
return path;
}
@Override
Path input(final Path in) {
final String hash = Hashing.sha256(in);
this.used.add(hash);
return super.input(in, hash);
}
void write(final boolean clean) {
if (!clean) {
super.write();
return;
}
final Iterator<Map.Entry<String, String>> it = this.state.hashes.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry<String, String> next = it.next();
if (this.used.contains(next.getKey())) {
continue;
}
// Remove unused mapped file
it.remove();
final Path file = this.dir().resolve(next.getValue());
try {
Files.deleteIfExists(file);
} catch (final IOException ex) {
LOGGER.warn("Failed to delete no longer needed cached jar '{}'", file, ex);
}
}
super.write();
}
}