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.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.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.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class FileDownload { static final Logger LOGGER = LogUtils.getLogger(); volatile boolean cancelled; volatile boolean finished; volatile boolean error; volatile boolean extracting; @Nullable private volatile File tempFile; volatile File resourcePackPath; @Nullable private volatile HttpGet request; @Nullable private Thread currentThread; private final RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(120000).setConnectTimeout(120000).build(); 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" }; public long contentLength(String uri) { CloseableHttpClient closeableHttpClient = null; HttpGet httpGet = null; long var5; try { httpGet = new HttpGet(uri); closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build(); CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet); return Long.parseLong(closeableHttpResponse.getFirstHeader("Content-Length").getValue()); } catch (Throwable var16) { LOGGER.error("Unable to get content length for download"); var5 = 0L; } finally { if (httpGet != null) { httpGet.releaseConnection(); } if (closeableHttpClient != null) { try { closeableHttpClient.close(); } catch (IOException var15) { LOGGER.error("Could not close http client", (Throwable)var15); } } } return var5; } public void download(WorldDownload download, String worldName, RealmsDownloadLatestWorldScreen.DownloadStatus status, LevelStorageSource source) { if (this.currentThread == null) { this.currentThread = new Thread( () -> { CloseableHttpClient closeableHttpClient = null; try { this.tempFile = File.createTempFile("backup", ".tar.gz"); this.request = new HttpGet(download.downloadLink); closeableHttpClient = HttpClientBuilder.create().setDefaultRequestConfig(this.requestConfig).build(); HttpResponse httpResponse = closeableHttpClient.execute(this.request); status.totalBytes = Long.parseLong(httpResponse.getFirstHeader("Content-Length").getValue()); if (httpResponse.getStatusLine().getStatusCode() == 200) { OutputStream outputStream2 = new FileOutputStream(this.tempFile); FileDownload.ProgressListener progressListener = new FileDownload.ProgressListener(worldName.trim(), this.tempFile, source, status); FileDownload.DownloadCountingOutputStream downloadCountingOutputStream2 = new FileDownload.DownloadCountingOutputStream(outputStream2); downloadCountingOutputStream2.setListener(progressListener); IOUtils.copy(httpResponse.getEntity().getContent(), downloadCountingOutputStream2); return; } this.error = true; this.request.abort(); } catch (Exception var93) { LOGGER.error("Caught exception while downloading: {}", var93.getMessage()); this.error = true; return; } finally { this.request.releaseConnection(); if (this.tempFile != null) { this.tempFile.delete(); } if (!this.error) { if (!download.resourcePackUrl.isEmpty() && !download.resourcePackHash.isEmpty()) { try { this.tempFile = File.createTempFile("resources", ".tar.gz"); this.request = new HttpGet(download.resourcePackUrl); HttpResponse httpResponse3 = closeableHttpClient.execute(this.request); status.totalBytes = Long.parseLong(httpResponse3.getFirstHeader("Content-Length").getValue()); if (httpResponse3.getStatusLine().getStatusCode() != 200) { this.error = true; this.request.abort(); return; } OutputStream outputStream3 = new FileOutputStream(this.tempFile); FileDownload.ResourcePackProgressListener resourcePackProgressListener3 = new FileDownload.ResourcePackProgressListener( this.tempFile, status, download ); FileDownload.DownloadCountingOutputStream downloadCountingOutputStream3 = new FileDownload.DownloadCountingOutputStream(outputStream3); downloadCountingOutputStream3.setListener(resourcePackProgressListener3); IOUtils.copy(httpResponse3.getEntity().getContent(), downloadCountingOutputStream3); } catch (Exception var91) { LOGGER.error("Caught exception while downloading: {}", var91.getMessage()); this.error = true; } finally { this.request.releaseConnection(); if (this.tempFile != null) { this.tempFile.delete(); } } } else { this.finished = true; } } if (closeableHttpClient != null) { try { closeableHttpClient.close(); } catch (IOException var90) { LOGGER.error("Failed to close Realms download client"); } } } } ); this.currentThread.setUncaughtExceptionHandler(new RealmsDefaultUncaughtExceptionHandler(LOGGER)); this.currentThread.start(); } } public void cancel() { if (this.request != null) { this.request.abort(); } if (this.tempFile != null) { this.tempFile.delete(); } this.cancelled = true; } public boolean isFinished() { return this.finished; } public boolean isError() { return this.error; } public boolean isExtracting() { return this.extracting; } /** * Modifies a folder name to make sure it is valid to store on disk. * @return the modified folder name * * @param folderName The folder name to modify */ public static String findAvailableFolderName(String folderName) { folderName = folderName.replaceAll("[\\./\"]", "_"); for (String string : INVALID_FILE_NAMES) { if (folderName.equalsIgnoreCase(string)) { folderName = "_" + folderName + "_"; } } return folderName; } void untarGzipArchive(String worldName, @Nullable File tempFile, LevelStorageSource levelStorageSource) throws IOException { Pattern pattern = Pattern.compile(".*-([0-9]+)$"); int i = 1; for (char c : SharedConstants.ILLEGAL_FILE_CHARACTERS) { worldName = worldName.replace(c, '_'); } if (StringUtils.isEmpty(worldName)) { worldName = "Realm"; } worldName = findAvailableFolderName(worldName); try { for (LevelStorageSource.LevelDirectory levelDirectory : levelStorageSource.findLevelCandidates()) { String string = levelDirectory.directoryName(); if (string.toLowerCase(Locale.ROOT).startsWith(worldName.toLowerCase(Locale.ROOT))) { Matcher matcher = pattern.matcher(string); if (matcher.matches()) { int j = Integer.parseInt(matcher.group(1)); if (j > i) { i = j; } } else { i++; } } } } catch (Exception var43) { LOGGER.error("Error getting level list", (Throwable)var43); this.error = true; return; } String string2; if (levelStorageSource.isNewLevelIdAcceptable(worldName) && i <= 1) { string2 = worldName; } else { string2 = worldName + (i == 1 ? "" : "-" + i); if (!levelStorageSource.isNewLevelIdAcceptable(string2)) { boolean bl = false; while (!bl) { i++; string2 = worldName + (i == 1 ? "" : "-" + i); if (levelStorageSource.isNewLevelIdAcceptable(string2)) { bl = true; } } } } TarArchiveInputStream tarArchiveInputStream = null; File file = new File(Minecraft.getInstance().gameDirectory.getAbsolutePath(), "saves"); try { file.mkdir(); tarArchiveInputStream = new TarArchiveInputStream(new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(tempFile)))); for (TarArchiveEntry tarArchiveEntry = tarArchiveInputStream.getNextTarEntry(); tarArchiveEntry != null; tarArchiveEntry = tarArchiveInputStream.getNextTarEntry() ) { File file2 = new File(file, tarArchiveEntry.getName().replace("world", string2)); if (tarArchiveEntry.isDirectory()) { file2.mkdirs(); } else { file2.createNewFile(); FileOutputStream fileOutputStream = new FileOutputStream(file2); try { IOUtils.copy(tarArchiveInputStream, fileOutputStream); } catch (Throwable var37) { try { fileOutputStream.close(); } catch (Throwable var36) { var37.addSuppressed(var36); } throw var37; } fileOutputStream.close(); } } } catch (Exception var41) { LOGGER.error("Error extracting world", (Throwable)var41); this.error = true; } finally { if (tarArchiveInputStream != null) { tarArchiveInputStream.close(); } if (tempFile != null) { tempFile.delete(); } try (LevelStorageSource.LevelStorageAccess levelStorageAccess2 = levelStorageSource.validateAndCreateAccess(string2)) { levelStorageAccess2.renameAndDropPlayer(string2); } catch (NbtException | ReportedNbtException | IOException var39) { LOGGER.error("Failed to modify unpacked realms level {}", string2, var39); } catch (ContentValidationException var40) { LOGGER.warn("{}", var40.getMessage()); } this.resourcePackPath = new File(file, string2 + File.separator + "resources.zip"); } } @Environment(EnvType.CLIENT) static class DownloadCountingOutputStream extends CountingOutputStream { @Nullable private ActionListener listener; public DownloadCountingOutputStream(OutputStream out) { super(out); } public void setListener(ActionListener listener) { this.listener = listener; } @Override protected void afterWrite(int i) throws IOException { super.afterWrite(i); if (this.listener != null) { this.listener.actionPerformed(new ActionEvent(this, 0, null)); } } } @Environment(EnvType.CLIENT) class ProgressListener implements ActionListener { private final String worldName; private final File tempFile; private final LevelStorageSource levelStorageSource; private final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus; ProgressListener( final String worldName, final File tempFile, final LevelStorageSource levelStorageSource, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus ) { this.worldName = worldName; this.tempFile = tempFile; this.levelStorageSource = levelStorageSource; this.downloadStatus = downloadStatus; } public void actionPerformed(ActionEvent actionEvent) { this.downloadStatus.bytesWritten = ((FileDownload.DownloadCountingOutputStream)actionEvent.getSource()).getByteCount(); if (this.downloadStatus.bytesWritten >= this.downloadStatus.totalBytes && !FileDownload.this.cancelled && !FileDownload.this.error) { try { FileDownload.this.extracting = true; FileDownload.this.untarGzipArchive(this.worldName, this.tempFile, this.levelStorageSource); } catch (IOException var3) { FileDownload.LOGGER.error("Error extracting archive", (Throwable)var3); FileDownload.this.error = true; } } } } @Environment(EnvType.CLIENT) class ResourcePackProgressListener implements ActionListener { private final File tempFile; private final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus; private final WorldDownload worldDownload; ResourcePackProgressListener(final File tempFile, final RealmsDownloadLatestWorldScreen.DownloadStatus downloadStatus, final WorldDownload worldDownload) { this.tempFile = tempFile; this.downloadStatus = downloadStatus; this.worldDownload = worldDownload; } public void actionPerformed(ActionEvent actionEvent) { this.downloadStatus.bytesWritten = ((FileDownload.DownloadCountingOutputStream)actionEvent.getSource()).getByteCount(); if (this.downloadStatus.bytesWritten >= this.downloadStatus.totalBytes && !FileDownload.this.cancelled) { try { String string = Hashing.sha1().hashBytes(Files.toByteArray(this.tempFile)).toString(); if (string.equals(this.worldDownload.resourcePackHash)) { FileUtils.copyFile(this.tempFile, FileDownload.this.resourcePackPath); FileDownload.this.finished = true; } else { FileDownload.LOGGER.error("Resourcepack had wrong hash (expected {}, found {}). Deleting it.", this.worldDownload.resourcePackHash, string); FileUtils.deleteQuietly(this.tempFile); FileDownload.this.error = true; } } catch (IOException var3) { FileDownload.LOGGER.error("Error copying resourcepack file: {}", var3.getMessage()); FileDownload.this.error = true; } } } } }