218 lines
7.7 KiB
Java
218 lines
7.7 KiB
Java
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.io.UncheckedIOException;
|
|
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 UncheckedIOException(ex);
|
|
}
|
|
}
|
|
|
|
this.indexFile = dir.resolve(INDEX_FILE_NAME);
|
|
if (Files.isRegularFile(this.indexFile)) {
|
|
this.state = this.readIndex();
|
|
} else {
|
|
this.state = new State();
|
|
}
|
|
}
|
|
|
|
private State readIndex() {
|
|
final State state;
|
|
try (final BufferedReader reader = Files.newBufferedReader(this.indexFile)) {
|
|
state = GSON.fromJson(reader, State.class);
|
|
} catch (final Exception ex) {
|
|
throw new RuntimeException("Failed to read index file '" + this.indexFile + "'", ex);
|
|
}
|
|
|
|
// 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()) {
|
|
final Path path = this.dir.resolve(fileName);
|
|
try {
|
|
Files.deleteIfExists(path);
|
|
} catch (final IOException ex) {
|
|
throw new UncheckedIOException("Failed to delete no longer needed file '" + path + "'", ex);
|
|
}
|
|
}
|
|
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();
|
|
final Path filePath = this.dir.resolve(fileName);
|
|
try {
|
|
Files.deleteIfExists(filePath);
|
|
} catch (final IOException ex) {
|
|
throw new UncheckedIOException("Failed to delete no longer needed file '" + filePath + "'", 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);
|
|
}
|
|
}
|
|
}
|