package net.minecraft.data; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.mojang.logging.LogUtils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import net.minecraft.WorldVersion; import org.apache.commons.lang3.mutable.MutableInt; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class HashCache { static final Logger LOGGER = LogUtils.getLogger(); private static final String HEADER_MARKER = "// "; private final Path rootDir; private final Path cacheDir; private final String versionId; private final Map caches; private final Set cachesToWrite = new HashSet(); final Set cachePaths = new HashSet(); private final int initialCount; private int writes; private Path getProviderCachePath(String provider) { return this.cacheDir.resolve(Hashing.sha1().hashString(provider, StandardCharsets.UTF_8).toString()); } public HashCache(Path rootDir, Collection providers, WorldVersion version) throws IOException { this.versionId = version.getName(); this.rootDir = rootDir; this.cacheDir = rootDir.resolve(".cache"); Files.createDirectories(this.cacheDir); Map map = new HashMap(); int i = 0; for (String string : providers) { Path path = this.getProviderCachePath(string); this.cachePaths.add(path); HashCache.ProviderCache providerCache = readCache(rootDir, path); map.put(string, providerCache); i += providerCache.count(); } this.caches = map; this.initialCount = i; } private static HashCache.ProviderCache readCache(Path rootDir, Path cachePath) { if (Files.isReadable(cachePath)) { try { return HashCache.ProviderCache.load(rootDir, cachePath); } catch (Exception var3) { LOGGER.warn("Failed to parse cache {}, discarding", cachePath, var3); } } return new HashCache.ProviderCache("unknown", ImmutableMap.of()); } public boolean shouldRunInThisVersion(String provider) { HashCache.ProviderCache providerCache = (HashCache.ProviderCache)this.caches.get(provider); return providerCache == null || !providerCache.version.equals(this.versionId); } public CompletableFuture generateUpdate(String provider, HashCache.UpdateFunction updateFunction) { HashCache.ProviderCache providerCache = (HashCache.ProviderCache)this.caches.get(provider); if (providerCache == null) { throw new IllegalStateException("Provider not registered: " + provider); } else { HashCache.CacheUpdater cacheUpdater = new HashCache.CacheUpdater(provider, this.versionId, providerCache); return updateFunction.update(cacheUpdater).thenApply(object -> cacheUpdater.close()); } } public void applyUpdate(HashCache.UpdateResult updateResult) { this.caches.put(updateResult.providerId(), updateResult.cache()); this.cachesToWrite.add(updateResult.providerId()); this.writes = this.writes + updateResult.writes(); } /** * Writes the cache file containing the hashes of newly created files to the disk, and deletes any stale files. */ public void purgeStaleAndWrite() throws IOException { final Set set = new HashSet(); this.caches.forEach((string, providerCache) -> { if (this.cachesToWrite.contains(string)) { Path path = this.getProviderCachePath(string); providerCache.save(this.rootDir, path, DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) + "\t" + string); } set.addAll(providerCache.data().keySet()); }); set.add(this.rootDir.resolve("version.json")); final MutableInt mutableInt = new MutableInt(); final MutableInt mutableInt2 = new MutableInt(); Files.walkFileTree(this.rootDir, new SimpleFileVisitor() { public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) { if (HashCache.this.cachePaths.contains(path)) { return FileVisitResult.CONTINUE; } else { mutableInt.increment(); if (set.contains(path)) { return FileVisitResult.CONTINUE; } else { try { Files.delete(path); } catch (IOException var4) { HashCache.LOGGER.warn("Failed to delete file {}", path, var4); } mutableInt2.increment(); return FileVisitResult.CONTINUE; } } } }); LOGGER.info( "Caching: total files: {}, old count: {}, new count: {}, removed stale: {}, written: {}", mutableInt, this.initialCount, set.size(), mutableInt2, this.writes ); } static class CacheUpdater implements CachedOutput { private final String provider; private final HashCache.ProviderCache oldCache; private final HashCache.ProviderCacheBuilder newCache; private final AtomicInteger writes = new AtomicInteger(); private volatile boolean closed; CacheUpdater(String provider, String version, HashCache.ProviderCache oldCache) { this.provider = provider; this.oldCache = oldCache; this.newCache = new HashCache.ProviderCacheBuilder(version); } private boolean shouldWrite(Path key, HashCode value) { return !Objects.equals(this.oldCache.get(key), value) || !Files.exists(key, new LinkOption[0]); } @Override public void writeIfNeeded(Path path, byte[] bs, HashCode hashCode) throws IOException { if (this.closed) { throw new IllegalStateException("Cannot write to cache as it has already been closed"); } else { if (this.shouldWrite(path, hashCode)) { this.writes.incrementAndGet(); Files.createDirectories(path.getParent()); Files.write(path, bs, new OpenOption[0]); } this.newCache.put(path, hashCode); } } public HashCache.UpdateResult close() { this.closed = true; return new HashCache.UpdateResult(this.provider, this.newCache.build(), this.writes.get()); } } record ProviderCache(String version, ImmutableMap data) { @Nullable public HashCode get(Path path) { return this.data.get(path); } public int count() { return this.data.size(); } public static HashCache.ProviderCache load(Path rootDir, Path cachePath) throws IOException { BufferedReader bufferedReader = Files.newBufferedReader(cachePath, StandardCharsets.UTF_8); HashCache.ProviderCache var7; try { String string = bufferedReader.readLine(); if (!string.startsWith("// ")) { throw new IllegalStateException("Missing cache file header"); } String[] strings = string.substring("// ".length()).split("\t", 2); String string2 = strings[0]; Builder builder = ImmutableMap.builder(); bufferedReader.lines().forEach(stringx -> { int i = stringx.indexOf(32); builder.put(rootDir.resolve(stringx.substring(i + 1)), HashCode.fromString(stringx.substring(0, i))); }); var7 = new HashCache.ProviderCache(string2, builder.build()); } catch (Throwable var9) { if (bufferedReader != null) { try { bufferedReader.close(); } catch (Throwable var8) { var9.addSuppressed(var8); } } throw var9; } if (bufferedReader != null) { bufferedReader.close(); } return var7; } public void save(Path rootDir, Path cachePath, String date) { try { BufferedWriter bufferedWriter = Files.newBufferedWriter(cachePath, StandardCharsets.UTF_8); try { bufferedWriter.write("// "); bufferedWriter.write(this.version); bufferedWriter.write(9); bufferedWriter.write(date); bufferedWriter.newLine(); for (Entry entry : this.data.entrySet()) { bufferedWriter.write(((HashCode)entry.getValue()).toString()); bufferedWriter.write(32); bufferedWriter.write(rootDir.relativize((Path)entry.getKey()).toString()); bufferedWriter.newLine(); } } catch (Throwable var8) { if (bufferedWriter != null) { try { bufferedWriter.close(); } catch (Throwable var7) { var8.addSuppressed(var7); } } throw var8; } if (bufferedWriter != null) { bufferedWriter.close(); } } catch (IOException var9) { HashCache.LOGGER.warn("Unable write cachefile {}: {}", cachePath, var9); } } } record ProviderCacheBuilder(String version, ConcurrentMap data) { ProviderCacheBuilder(String version) { this(version, new ConcurrentHashMap()); } public void put(Path key, HashCode value) { this.data.put(key, value); } public HashCache.ProviderCache build() { return new HashCache.ProviderCache(this.version, ImmutableMap.copyOf(this.data)); } } @FunctionalInterface public interface UpdateFunction { CompletableFuture update(CachedOutput cachedOutput); } public record UpdateResult(String providerId, HashCache.ProviderCache cache, int writes) { } }