package com.faforever.client.patch; import com.faforever.client.i18n.I18n; import com.faforever.client.os.OperatingSystem; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.task.CompletableTask; import com.google.common.hash.Hashing; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.sigpipe.jbsdiff.InvalidHeaderException; import io.sigpipe.jbsdiff.Patch; import org.apache.commons.compress.compressors.CompressorException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Map; import java.util.Set; public class GitGameUpdateTask extends CompletableTask<Void> { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String BINARY_PATCH_DIRECTORY = "bsdiff4"; private final Gson gson; @Resource I18n i18n; @Resource PreferencesService preferencesService; @Resource GitWrapper gitWrapper; @Resource Environment environment; private Path binaryPatchRepoDirectory; private String patchRepositoryUri; private Path migrationDataFile; public GitGameUpdateTask() { super(Priority.MEDIUM); gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create(); } @PostConstruct void postConstruct() { updateTitle(i18n.get("patchTask.title")); patchRepositoryUri = environment.getProperty("patch.git.url"); } @Override protected Void call() throws Exception { if (Files.notExists(binaryPatchRepoDirectory)) { clonePatchRepository(); } try (BufferedReader reader = Files.newBufferedReader(migrationDataFile, StandardCharsets.UTF_8)) { MigrationData migrationData = gson.fromJson(reader, MigrationData.class); copyGameFilesToFafBinDirectory(migrationData); Set<Map.Entry<String, String>> entries = migrationData.postPatchVerify.entrySet(); long progress = 0; updateProgress(progress, entries.size()); for (Map.Entry<String, String> entry : entries) { String fileName = entry.getKey(); String expectedMd5AfterPatch = entry.getValue(); Path fileToPatch = preferencesService.getFafBinDirectory().resolve(fileName); byte[] bytesOfFileToPatch = Files.readAllBytes(fileToPatch); Path patchFile = getPatchFile(bytesOfFileToPatch); if (Files.notExists(patchFile)) { updateProgress(++progress, entries.size()); continue; } patchFile(fileToPatch, bytesOfFileToPatch, patchFile); verifyPatchedFile(expectedMd5AfterPatch, fileToPatch); logger.info("Patching successful for file: {}", fileToPatch); updateProgress(++progress, entries.size()); } } logger.info("All files have been patched successfully"); return null; } private void clonePatchRepository() { gitWrapper.clone(patchRepositoryUri, binaryPatchRepoDirectory); } protected void copyGameFilesToFafBinDirectory(MigrationData migrationData) throws IOException { Path fafBinDirectory = preferencesService.getFafBinDirectory(); Files.createDirectories(fafBinDirectory); for (Map.Entry<String, String> entry : migrationData.prePatchCopyRename.entrySet()) { String oldName = entry.getKey(); String newName = entry.getValue() != null ? entry.getValue() : oldName; Path source = preferencesService.getPreferences().getForgedAlliance().getPath().resolve("bin").resolve(oldName); Path destination = fafBinDirectory.resolve(newName); logger.debug("Copying file '{}' to '{}'", source, destination); Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); if (OperatingSystem.current() == OperatingSystem.WINDOWS) { Files.setAttribute(destination, "dos:readonly", false); } } } private Path getPatchFile(byte[] bytesOfFileToPatch) { Path patchSourceDirectory = binaryPatchRepoDirectory.resolve(BINARY_PATCH_DIRECTORY); return patchSourceDirectory.resolve(Hashing.md5().hashBytes(bytesOfFileToPatch).toString()); } private void patchFile(Path fileToPatch, byte[] bytesOfFileToPatch, Path patchFile) throws IOException, CompressorException, InvalidHeaderException { logger.info("Patching file {}", fileToPatch); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(fileToPatch))) { Patch.patch( bytesOfFileToPatch, Files.readAllBytes(patchFile), outputStream ); } } private void verifyPatchedFile(String expectedMd5AfterPatch, Path fileToPatch) throws IOException { String md5OfPatchedFile = Hashing.md5().hashBytes(Files.readAllBytes(fileToPatch)).toString(); if (!md5OfPatchedFile.equals(expectedMd5AfterPatch)) { throw new PatchingFailedException( String.format("Patching failed for file: '%s'. Expected checksum: %s but got: %s", fileToPatch, expectedMd5AfterPatch, md5OfPatchedFile) ); } } public void setBinaryPatchRepoDirectory(Path binaryPatchRepoDirectory) { this.binaryPatchRepoDirectory = binaryPatchRepoDirectory; } public void setMigrationDataFile(Path migrationDataFile) { this.migrationDataFile = migrationDataFile; } }