diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java index 0f2121afb..5ac3ac423 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java @@ -62,9 +62,7 @@ public final class GameLibrariesTask extends Task { version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> { File file = dependencyManager.getGameRepository().getLibraryFile(version, library); if (!file.exists()) - dependencies.add(new FileDownloadTask( - NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())), - file, dependencyManager.getProxy(), library.getDownload().getSha1())); + dependencies.add(new LibraryDownloadTask(dependencyManager, file, library)); }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java new file mode 100644 index 000000000..86052d1a7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -0,0 +1,161 @@ +package org.jackhuang.hmcl.download.game; + +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.jackhuang.hmcl.download.AbstractDependencyManager; +import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Scheduler; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.*; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Pack200; + +public final class LibraryDownloadTask extends Task { + private final FileDownloadTask xzTask; + private final FileDownloadTask task; + private final File jar; + private final File xzFile; + private final Library library; + + public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) { + String url = dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl()); + jar = file; + this.library = library; + xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz"); + + xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"), + xzFile, dependencyManager.getProxy(), null, 1); + + task = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"), + file, dependencyManager.getProxy(), library.getDownload().getSha1()); + } + + @Override + public Collection getDependents() { + return Collections.singleton(xzTask); + } + + @Override + public boolean isRelyingOnDependents() { + return false; + } + + @Override + public Scheduler getScheduler() { + return Schedulers.io(); + } + + @Override + public void execute() throws Exception { + if (isDependentsSucceeded()) { + unpackLibrary(jar, FileUtils.readBytes(xzFile)); + if (!checksumValid(jar, library.getChecksums())) + throw new IOException("Checksum failed for " + library); + } + } + + @Override + public Collection getDependencies() { + return isDependentsSucceeded() ? Collections.emptySet() : Collections.singleton(task); + } + + private static boolean checksumValid(File libPath, List checksums) { + try { + if ((checksums == null) || (checksums.isEmpty())) { + return true; + } + byte[] fileData = FileUtils.readBytes(libPath); + boolean valid = checksums.contains(DigestUtils.sha1Hex(fileData)); + if ((!valid) && (libPath.getName().endsWith(".jar"))) { + } + return validateJar(fileData, checksums); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + private static boolean validateJar(byte[] data, List checksums) throws IOException { + HashMap files = new HashMap<>(); + String[] hashes = null; + JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data)); + JarEntry entry = jar.getNextJarEntry(); + while (entry != null) { + byte[] eData = IOUtils.readFullyAsByteArray(jar); + if (entry.getName().equals("checksums.sha1")) { + hashes = new String(eData, Charset.forName("UTF-8")).split("\n"); + } + if (!entry.isDirectory()) { + files.put(entry.getName(), DigestUtils.sha1Hex(eData)); + } + entry = jar.getNextJarEntry(); + } + jar.close(); + if (hashes != null) { + boolean passed = !checksums.contains(files.get("checksums.sha1")); + if (passed) { + for (String hash : hashes) { + if ((!hash.trim().equals("")) && (hash.contains(" "))) { + String[] e = hash.split(" "); + String validChecksum = e[0]; + String target = hash.substring(validChecksum.length() + 1); + String checksum = files.get(target); + if ((!files.containsKey(target)) || (checksum == null)) { + Logging.LOG.warning(" " + target + " : missing"); + passed = false; + break; + } else if (!checksum.equals(validChecksum)) { + Logging.LOG.warning(" " + target + " : failed (" + checksum + ", " + validChecksum + ")"); + passed = false; + break; + } + } + } + } + return passed; + } + return false; + } + + private static void unpackLibrary(File dest, byte[] src) throws IOException { + if (dest.exists()) + if (!dest.delete()) + throw new IOException("Unable to delete file " + dest); + + byte[] decompressed = IOUtils.readFullyAsByteArray(new XZCompressorInputStream(new ByteArrayInputStream(src))); + + String end = new String(decompressed, decompressed.length - 4, 4); + if (!end.equals("SIGN")) + throw new IOException("Unpacking failed, signature missing " + end); + + int x = decompressed.length; + int len = decompressed[(x - 8)] & 0xFF | (decompressed[(x - 7)] & 0xFF) << 8 | (decompressed[(x - 6)] & 0xFF) << 16 | (decompressed[(x - 5)] & 0xFF) << 24; + + File temp = FileUtils.createTempFile("minecraft", ".pack"); + + byte[] checksums = Arrays.copyOfRange(decompressed, decompressed.length - len - 8, decompressed.length - 8); + + OutputStream out = new FileOutputStream(temp); + out.write(decompressed, 0, decompressed.length - len - 8); + out.close(); + + try (FileOutputStream jarBytes = new FileOutputStream(dest); JarOutputStream jos = new JarOutputStream(jarBytes)) { + Pack200.newUnpacker().unpack(temp, jos); + + JarEntry checksumsFile = new JarEntry("checksums.sha1"); + checksumsFile.setTime(0L); + jos.putNextEntry(checksumsFile); + jos.write(checksums); + jos.closeEntry(); + } + + temp.delete(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java index da8515ae4..805eb4138 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java @@ -40,7 +40,7 @@ public class ClassicVersion extends Version { public ClassicLibrary(String name) { super("", "", "", null, null, new LibrariesDownloadInfo(new LibraryDownloadInfo("bin/" + name + ".jar"), null), - false, null, null, null); + false, null, null, null, null); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java index eb1af8268..1de840576 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -48,6 +48,7 @@ public class Library implements Comparable { private final boolean lateload; private final Map natives; private final List rules; + private final List checksums; private final String path; @@ -60,10 +61,10 @@ public class Library implements Comparable { } public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, boolean lateload) { - this(groupId, artifactId, version, classifier, url, downloads, lateload, null, null, null); + this(groupId, artifactId, version, classifier, url, downloads, lateload, null, null, null, null); } - public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, boolean lateload, ExtractRules extract, Map natives, List rules) { + public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, boolean lateload, List checksums, ExtractRules extract, Map natives, List rules) { this.groupId = groupId; this.artifactId = artifactId; this.version = version; @@ -80,6 +81,7 @@ public class Library implements Comparable { this.lateload = lateload; this.natives = natives; this.rules = rules; + this.checksums = checksums; LibraryDownloadInfo temp = null; if (downloads != null) @@ -141,6 +143,10 @@ public class Library implements Comparable { return lateload; } + public List getChecksums() { + return checksums; + } + @Override public String toString() { return new ToStringBuilder(this).append("name", getName()).toString(); @@ -169,15 +175,15 @@ public class Library implements Comparable { } public static Library fromName(String name) { - return fromName(name, null, null, null, null, null); + return fromName(name, null, null, null, null, null, null); } - public static Library fromName(String name, String url, LibrariesDownloadInfo downloads, ExtractRules extract, Map natives, List rules) { + public static Library fromName(String name, String url, LibrariesDownloadInfo downloads, List checksums, ExtractRules extract, Map natives, List rules) { String[] arr = name.split(":", 4); if (arr.length != 3 && arr.length != 4) throw new IllegalArgumentException("Library name is malformed. Correct example: group:artifact:version(:classifier)."); - return new Library(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, url, downloads, false, extract, natives, rules); + return new Library(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, url, downloads, false, checksums, extract, natives, rules); } public static class Serializer implements JsonDeserializer, JsonSerializer { @@ -198,6 +204,8 @@ public class Library implements Comparable { jsonObject.get("name").getAsString(), jsonObject.has("url") ? jsonObject.get("url").getAsString() : null, context.deserialize(jsonObject.get("downloads"), LibrariesDownloadInfo.class), + context.deserialize(jsonObject.get("checksums"), new TypeToken>() { + }.getType()), context.deserialize(jsonObject.get("extract"), ExtractRules.class), context.deserialize(jsonObject.get("natives"), new TypeToken>() { }.getType()), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index 2bb7d7361..a77a4d74a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -153,7 +153,10 @@ public final class TaskExecutor { try { task.getScheduler().schedule(task::execute).get(); } catch (ExecutionException e) { - throw (Exception) e.getCause(); + if (e.getCause() instanceof Exception) + throw (Exception) e.getCause(); + else + throw e; } if (task instanceof TaskResult) { diff --git a/build.gradle b/build.gradle index 4aef48a72..c1d64b51c 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,7 @@ allprojects { dependencies { compile "com.google.code.gson:gson:2.8.2" compile "org.apache.commons:commons-compress:1.15" + compile "org.tukaani:xz:1.2" testCompile group: 'junit', name: 'junit', version: '4.12' }