diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java index 01e8a32f5a..b27aae4df8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java @@ -19,21 +19,15 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; import java.util.Optional; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import org.jackhuang.hmcl.util.io.IOUtils; +import kala.compress.archivers.zip.ZipArchiveEntry; +import kala.compress.archivers.zip.ZipArchiveReader; +import kala.compress.utils.SeekableInMemoryByteChannel; import static java.nio.file.StandardOpenOption.*; -import static org.jackhuang.hmcl.util.Lang.mapOf; -import static org.jackhuang.hmcl.util.Pair.pair; /** * Helper class for adding/removing executable header from HMCL file. @@ -41,84 +35,47 @@ * @author yushijinhun */ final class ExecutableHeaderHelper { - private ExecutableHeaderHelper() {} - - private static Map suffix2header = mapOf( - pair("exe", "assets/HMCLauncher.exe"), - pair("sh", "assets/HMCLauncher.sh") - ); + private ExecutableHeaderHelper() { + } private static Optional getSuffix(Path file) { String filename = file.getFileName().toString(); int idxDot = filename.lastIndexOf('.'); - if (idxDot < 0) { - return Optional.empty(); - } else { - return Optional.of(filename.substring(idxDot + 1)); - } + return idxDot >= 0 + ? Optional.of(filename.substring(idxDot + 1)) + : Optional.empty(); } - private static Optional readHeader(ZipFile zip, String suffix) throws IOException { - String location = suffix2header.get(suffix); - if (location != null) { - ZipEntry entry = zip.getEntry(location); - if (entry != null && !entry.isDirectory()) { - try (InputStream in = zip.getInputStream(entry)) { - return Optional.of(IOUtils.readFully(in)); - } + private static Optional readHeader(ZipArchiveReader zip, String suffix) throws IOException { + String location = "assets/HMCLauncher." + suffix; + ZipArchiveEntry entry = zip.getEntry(location); + if (entry != null && !entry.isDirectory()) { + try (InputStream in = zip.getInputStream(entry)) { + return Optional.of(in.readAllBytes()); } } return Optional.empty(); } - private static int detectHeaderLength(ZipFile zip, FileChannel channel) throws IOException { - ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, channel.size()); - suffixLoop: for (String suffix : suffix2header.keySet()) { - Optional header = readHeader(zip, suffix); - if (header.isPresent()) { - ((Buffer) buf).rewind(); - for (byte b : header.get()) { - if (!buf.hasRemaining() || b != buf.get()) { - continue suffixLoop; - } - } - return header.get().length; - } - } - return 0; - } - - /** - * Copies the executable and removes its header. - */ - public static void copyWithoutHeader(Path from, Path to) throws IOException { - try ( - FileChannel in = FileChannel.open(from, READ); - FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); - ZipFile zip = new ZipFile(from.toFile()) - ) { - in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); - } - } - /** * Copies the executable and appends the header according to the suffix. */ public static void copyWithHeader(Path from, Path to) throws IOException { - try ( - FileChannel in = FileChannel.open(from, READ); - FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); - ZipFile zip = new ZipFile(from.toFile()) - ) { + byte[] source = Files.readAllBytes(from); + + Files.createDirectories(to.toAbsolutePath().normalize().getParent()); + try (var reader = new ZipArchiveReader(new SeekableInMemoryByteChannel(source)); + var output = Files.newOutputStream(to, CREATE, WRITE, TRUNCATE_EXISTING)) { Optional suffix = getSuffix(to); if (suffix.isPresent()) { - Optional header = readHeader(zip, suffix.get()); + Optional header = readHeader(reader, suffix.get()); if (header.isPresent()) { - out.write(ByteBuffer.wrap(header.get())); + output.write(header.get()); } } - in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); + final int offset = Math.toIntExact(reader.getFirstLocalFileHeaderOffset()); + output.write(source, offset, source.length - offset); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index c8ff9edac4..497700c5a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -21,6 +21,7 @@ import com.google.gson.JsonParseException; import javafx.application.Platform; +import org.intellij.lang.annotations.RegExp; import org.jackhuang.hmcl.EntryPoint; import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Metadata; @@ -35,6 +36,8 @@ import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -147,24 +150,23 @@ public static void updateFrom(RemoteVersion version) { private static void applyUpdate(Path target) throws IOException { LOG.info("Applying update to " + target); - Path self = getCurrentLocation(); if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) { throw new IOException("Self verification failed"); } - ExecutableHeaderHelper.copyWithHeader(self, target); - Optional newFilename = tryRename(target, Metadata.VERSION); - if (newFilename.isPresent()) { - LOG.info("Move " + target + " to " + newFilename.get()); + Path self = getCurrentLocation(); + Path newTarget = tryRename(target, UpdateChannel.getChannel(), Metadata.VERSION); + ExecutableHeaderHelper.copyWithHeader(self, newTarget); + if (!newTarget.equals(target)) { + LOG.info("Rename " + target + " to " + newTarget); try { - Files.move(target, newFilename.get()); - target = newFilename.get(); + Files.deleteIfExists(target); } catch (IOException e) { - LOG.warning("Failed to move target", e); + LOG.warning("Failed to delete old file " + target, e); } } - startJava(target); + startJava(newTarget); } private static void requestUpdate(Path updateTo, Path self) throws IOException { @@ -193,16 +195,33 @@ private static void startJava(Path jar, String... appArgs) throws IOException { .start(); } - private static Optional tryRename(Path path, String newVersion) { + private static @NotNull Path tryRename(@NotNull Path path, @NotNull UpdateChannel newChannel, @NotNull String newVersion) { String filename = path.getFileName().toString(); - Matcher matcher = Pattern.compile("^(?[hH][mM][cC][lL][.-])(?\\d+(?:\\.\\d+)*)(?\\.[^.]+)$").matcher(filename); - if (matcher.find()) { - String newFilename = matcher.group("prefix") + newVersion + matcher.group("suffix"); - if (!newFilename.equals(filename)) { - return Optional.of(path.resolveSibling(newFilename)); - } + String newFileName = tryRename(filename, newChannel, newVersion); + return newFileName != null ? path.resolveSibling(newFileName) : path; + } + + static @Nullable String tryRename(@NotNull String fileName, @NotNull UpdateChannel newChannel, @NotNull String newVersion) { + @RegExp final var prefixPattern = "[hH][mM][cC][lL]-"; + @RegExp final var channelPattern = "(?:dev|stable)-"; + @RegExp final var versionPattern = "\\d+(?:\\.\\d+)*(?:\\.(?:SNAPSHOT|[a-zA-Z0-9]+-[0-9a-f]{7}))?"; + @RegExp final var suffixPattern = "\\.(?:jar|exe|sh)"; + + Matcher matcher = Pattern.compile(String.format( + "^(?%s)(?%s)?(?%s)(?%s)$", + prefixPattern, channelPattern, versionPattern, suffixPattern)) + .matcher(fileName); + if (!matcher.matches()) { + return null; } - return Optional.empty(); + + StringBuilder builder = new StringBuilder(); + builder.append(matcher.group("prefix")); + if (matcher.group("channel") != null) + builder.append(newChannel.channelName).append('-'); + builder.append(newVersion); + builder.append(matcher.group("suffix")); + return builder.toString(); } private static Path getCurrentLocation() throws IOException { diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/upgrade/UpdateHandlerTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/upgrade/UpdateHandlerTest.java new file mode 100644 index 0000000000..a60c9d61d2 --- /dev/null +++ b/HMCL/src/test/java/org/jackhuang/hmcl/upgrade/UpdateHandlerTest.java @@ -0,0 +1,49 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.upgrade; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Glavo + */ +public final class UpdateHandlerTest { + + private static void assertRename(String expected, String fileName, UpdateChannel channel, String version) { + for (var ext : new String[]{".jar", ".exe", ".sh"}) { + assertEquals(expected + ext, UpdateHandler.tryRename(fileName + ext, channel, version)); + } + + assertNull(UpdateHandler.tryRename(fileName, channel, version)); + assertNull(UpdateHandler.tryRename(fileName + ".unknown", channel, version)); + } + + @Test + public void testRename() { + assertRename("HMCL-999.999.999", "HMCL-3.6.15.287", UpdateChannel.STABLE, "999.999.999"); + assertRename("HMCL-999.999.999", "HMCL-3.6.15", UpdateChannel.STABLE, "999.999.999"); + assertRename("HMCL-999.999.999", "HMCL-3.6.dev-3873459", UpdateChannel.STABLE, "999.999.999"); + assertRename("HMCL-999.999.999", "HMCL-3.6.dev-3873459", UpdateChannel.STABLE, "999.999.999"); + assertRename("HMCL-999.999.999", "HMCL-3.6.unofficial-3873459", UpdateChannel.STABLE, "999.999.999"); + assertRename("HMCL-999.999.999", "HMCL-3.6.SNAPSHOT", UpdateChannel.STABLE, "999.999.999"); + assertRename("hmcl-stable-999.999.999", "hmcl-dev-3.6.15.287", UpdateChannel.STABLE, "999.999.999"); + } +}