package com.mojang.realmsclient.client; import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.mojang.logging.LogUtils; import com.mojang.realmsclient.dto.WorldDownload; import com.mojang.realmsclient.exception.RealmsDefaultUncaughtExceptionHandler; import com.mojang.realmsclient.gui.screens.RealmsDownloadLatestWorldScreen; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpRequest.Builder; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Path; import java.time.Duration; import java.util.Locale; import java.util.OptionalLong; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckReturnValue; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.SharedConstants; import net.minecraft.client.Minecraft; import net.minecraft.nbt.NbtException; import net.minecraft.nbt.ReportedNbtException; import net.minecraft.util.FileUtil; import net.minecraft.util.Util; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.validation.ContentValidationException; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.CountingOutputStream; import org.apache.commons.lang3.StringUtils; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class FileDownload { private static final Logger LOGGER = LogUtils.getLogger(); private volatile boolean cancelled; private volatile boolean finished; private volatile boolean error; private volatile boolean extracting; @Nullable private volatile File tempFile; private volatile File resourcePackPath; @Nullable private volatile CompletableFuture pendingRequest; @Nullable private Thread currentThread; private static final String[] INVALID_FILE_NAMES = new String[]{ "CON", "COM", "PRN", "AUX", "CLOCK$", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; @Nullable private T joinCancellableRequest(final CompletableFuture pendingRequest) throws Throwable { this.pendingRequest = pendingRequest; if (this.cancelled) { pendingRequest.cancel(true); return null; } else { try { try { return (T)pendingRequest.join(); } catch (CompletionException var3) { throw var3.getCause(); } } catch (CancellationException var4) { return null; } } } private static HttpClient createClient() { return HttpClient.newBuilder().executor(Util.nonCriticalIoPool()).connectTimeout(Duration.ofMinutes(2L)).build(); } private static Builder createRequest(final String downloadLink) { return HttpRequest.newBuilder(URI.create(downloadLink)).timeout(Duration.ofMinutes(2L)); } @CheckReturnValue public static OptionalLong contentLength(final String downloadLink) { try { HttpClient client = createClient(); OptionalLong var3; try { HttpResponse response = client.send(createRequest(downloadLink).HEAD().build(), BodyHandlers.discarding()); var3 = response.headers().firstValueAsLong("Content-Length"); } catch (Throwable var5) { if (client != null) { try { client.close(); } catch (Throwable var4) { var5.addSuppressed(var4); } } throw var5; } if (client != null) { client.close(); } return var3; } catch (Exception var6) { LOGGER.error("Unable to get content length for download"); return OptionalLong.empty(); } } public void download( final WorldDownload worldDownload, final String worldName, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus, final LevelStorageSource levelStorageSource ) { if (this.currentThread == null) { this.currentThread = new Thread(() -> { HttpClient client = createClient(); label205: { try { try { this.tempFile = File.createTempFile("backup", ".tar.gz"); this.download(downloadStatus, client, worldDownload.downloadLink(), this.tempFile); this.finishWorldDownload(worldName.trim(), this.tempFile, levelStorageSource, downloadStatus); } catch (Exception var23) { LOGGER.error("Caught exception while downloading world", (Throwable)var23); this.error = true; } finally { this.pendingRequest = null; if (this.tempFile != null) { this.tempFile.delete(); } this.tempFile = null; } if (this.error) { break label205; } String resourcePackLink = worldDownload.resourcePackUrl(); if (!resourcePackLink.isEmpty() && !worldDownload.resourcePackHash().isEmpty()) { try { this.tempFile = File.createTempFile("resources", ".tar.gz"); this.download(downloadStatus, client, resourcePackLink, this.tempFile); this.finishResourcePackDownload(downloadStatus, this.tempFile, worldDownload); } catch (Exception var22) { LOGGER.error("Caught exception while downloading resource pack", (Throwable)var22); this.error = true; } finally { this.pendingRequest = null; if (this.tempFile != null) { this.tempFile.delete(); } this.tempFile = null; } } this.finished = true; } catch (Throwable var26) { if (client != null) { try { client.close(); } catch (Throwable var21) { var26.addSuppressed(var21); } } throw var26; } if (client != null) { client.close(); } return; } if (client != null) { client.close(); } }, "Realms world download"); this.currentThread.setUncaughtExceptionHandler(new RealmsDefaultUncaughtExceptionHandler(LOGGER)); this.currentThread.start(); } } private void download(final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus, final HttpClient client, final String url, final File target) throws IOException { HttpRequest request = createRequest(url).GET().build(); HttpResponse response; try { response = this.joinCancellableRequest(client.sendAsync(request, BodyHandlers.ofInputStream())); } catch (Error var14) { throw var14; } catch (Throwable var15) { LOGGER.error("Failed to download {}", url, var15); this.error = true; return; } if (response != null && !this.cancelled) { if (response.statusCode() != 200) { this.error = true; } else { downloadStatus.totalBytes = response.headers().firstValueAsLong("Content-Length").orElse(0L); InputStream is = (InputStream)response.body(); try { OutputStream os = new FileOutputStream(target); try { is.transferTo(new FileDownload.DownloadCountingOutputStream(os, downloadStatus)); } catch (Throwable var13) { try { os.close(); } catch (Throwable var12) { var13.addSuppressed(var12); } throw var13; } os.close(); } catch (Throwable var16) { if (is != null) { try { is.close(); } catch (Throwable var11) { var16.addSuppressed(var11); } } throw var16; } if (is != null) { is.close(); } } } } public void cancel() { if (this.tempFile != null) { this.tempFile.delete(); this.tempFile = null; } this.cancelled = true; CompletableFuture pendingRequest = this.pendingRequest; if (pendingRequest != null) { pendingRequest.cancel(true); } } public boolean isFinished() { return this.finished; } public boolean isError() { return this.error; } public boolean isExtracting() { return this.extracting; } public static String findAvailableFolderName(String folder) { folder = folder.replaceAll("[\\./\"]", "_"); for (String invalidName : INVALID_FILE_NAMES) { if (folder.equalsIgnoreCase(invalidName)) { folder = "_" + folder + "_"; } } return folder; } private void untarGzipArchive(String name, @Nullable final File file, final LevelStorageSource levelStorageSource) throws IOException { Pattern namePattern = Pattern.compile(".*-([0-9]+)$"); int number = 1; for (char replacer : SharedConstants.ILLEGAL_FILE_CHARACTERS) { name = name.replace(replacer, '_'); } if (StringUtils.isEmpty(name)) { name = "Realm"; } name = findAvailableFolderName(name); try { for (LevelStorageSource.LevelDirectory level : levelStorageSource.findLevelCandidates()) { String levelId = level.directoryName(); if (levelId.toLowerCase(Locale.ROOT).startsWith(name.toLowerCase(Locale.ROOT))) { Matcher matcher = namePattern.matcher(levelId); if (matcher.matches()) { int parsedNumber = Integer.parseInt(matcher.group(1)); if (parsedNumber > number) { number = parsedNumber; } } else { number++; } } } } catch (Exception var44) { LOGGER.error("Error getting level list", (Throwable)var44); this.error = true; return; } String finalName; if (levelStorageSource.isNewLevelIdAcceptable(name) && number <= 1) { finalName = name; } else { finalName = name + (number == 1 ? "" : "-" + number); if (!levelStorageSource.isNewLevelIdAcceptable(finalName)) { boolean foundName = false; while (!foundName) { number++; finalName = name + (number == 1 ? "" : "-" + number); if (levelStorageSource.isNewLevelIdAcceptable(finalName)) { foundName = true; } } } } TarArchiveInputStream tarIn = null; Path worldPath = Minecraft.getInstance().getLevelSource().getLevelPath(finalName).normalize(); try { FileUtil.createDirectoriesSafe(worldPath); tarIn = new TarArchiveInputStream(new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(file)))); TarArchiveEntry tarEntry = tarIn.getNextTarEntry(); while (tarEntry != null) { Path destPath = worldPath.resolve(Path.of("world").relativize(Path.of(tarEntry.getName()))).normalize(); if (!destPath.startsWith(worldPath)) { LOGGER.warn("Unexpected entry in Realms world download: {}", tarEntry.getName()); tarEntry = tarIn.getNextTarEntry(); } else { if (tarEntry.isDirectory()) { FileUtil.createDirectoriesSafe(destPath); } else { Path parent = destPath.getParent(); if (parent != null) { FileUtil.createDirectoriesSafe(parent); } FileOutputStream output = new FileOutputStream(destPath.toFile()); try { IOUtils.copy(tarIn, output); } catch (Throwable var38) { try { output.close(); } catch (Throwable var37) { var38.addSuppressed(var37); } throw var38; } output.close(); } tarEntry = tarIn.getNextTarEntry(); } } } catch (Exception var42) { LOGGER.error("Error extracting world", (Throwable)var42); this.error = true; } finally { if (tarIn != null) { tarIn.close(); } if (file != null) { file.delete(); } try (LevelStorageSource.LevelStorageAccess access = levelStorageSource.validateAndCreateAccess(finalName)) { access.renameAndDropPlayer(finalName); } catch (NbtException | ReportedNbtException | IOException var40) { LOGGER.error("Failed to modify unpacked realms level {}", finalName, var40); } catch (ContentValidationException var41) { LOGGER.warn("Failed to download file", (Throwable)var41); } this.resourcePackPath = worldPath.resolve(LevelResource.MAP_RESOURCE_FILE.id()).toFile(); } } private void finishWorldDownload( final String worldName, final File tempFile, final LevelStorageSource levelStorageSource, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus ) { if (downloadStatus.bytesWritten >= downloadStatus.totalBytes && !this.cancelled && !this.error) { try { this.extracting = true; this.untarGzipArchive(worldName, tempFile, levelStorageSource); } catch (IOException var6) { LOGGER.error("Error extracting archive", (Throwable)var6); this.error = true; } } } private void finishResourcePackDownload( final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus, final File tempFile, final WorldDownload worldDownload ) { if (downloadStatus.bytesWritten >= downloadStatus.totalBytes && !this.cancelled) { try { String actualHash = Hashing.sha1().hashBytes(Files.toByteArray(tempFile)).toString(); if (actualHash.equals(worldDownload.resourcePackHash())) { FileUtils.copyFile(tempFile, this.resourcePackPath); this.finished = true; } else { LOGGER.error("Resourcepack had wrong hash (expected {}, found {}). Deleting it.", worldDownload.resourcePackHash(), actualHash); FileUtils.deleteQuietly(tempFile); this.error = true; } } catch (IOException var5) { LOGGER.error("Error copying resourcepack file: {}", var5.getMessage()); this.error = true; } } } @Environment(EnvType.CLIENT) private static class DownloadCountingOutputStream extends CountingOutputStream { private final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus; public DownloadCountingOutputStream(final OutputStream out, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus) { super(out); this.downloadStatus = downloadStatus; } @Override protected void afterWrite(final int n) throws IOException { super.afterWrite(n); this.downloadStatus.bytesWritten = this.getByteCount(); } } }