diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java index 378d98ff2..2909268ef 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java @@ -23,12 +23,12 @@ import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.Path; +import java.nio.file.FileSystem; /** * @author huangyuhui @@ -40,20 +40,20 @@ public final class HMCLModpackManager { /** * Read the manifest in a HMCL modpack. * - * @param file a HMCL modpack file. + * @param fs a HMCL modpack file. * @param encoding encoding of modpack zip file. * @return the manifest of HMCL modpack. * @throws IOException if the file is not a valid zip file. * @throws JsonParseException if the manifest.json is missing or malformed. */ - public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException { - String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding); + public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException { + String manifestJson = FileUtils.readText(fs.getPath("modpack.json")); Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding); - String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding); + String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json")); Version game = JsonUtils.fromNonNullJson(gameJson, Version.class); if (game.getJar() == null) if (StringUtils.isBlank(manifest.getVersion())) - throw new JsonParseException("Cannot recognize the game version of modpack " + file + "."); + throw new JsonParseException("Cannot recognize the game version of modpack"); else manifest.setManifest(HMCLModpackManifest.INSTANCE); else diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 64a366234..9acc48bca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.ZipFileSystem; import java.io.File; import java.io.FileNotFoundException; @@ -61,34 +62,38 @@ public final class ModpackHelper { private ModpackHelper() {} public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException { - try { - return McbbsModpackManifest.readManifest(file, charset); - } catch (Exception ignored) { - // ignore it, not a valid MCBBS modpack. - } + try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) { + try { + return McbbsModpackManifest.readManifest(zfs, charset); + } catch (Exception ignored) { + // ignore it, not a valid MCBBS modpack. + } - try { - return CurseManifest.readCurseForgeModpackManifest(file, charset); - } catch (Exception e) { - // ignore it, not a valid CurseForge modpack. - } + try { + return CurseManifest.readCurseForgeModpackManifest(zfs, charset); + } catch (Exception e) { + // ignore it, not a valid CurseForge modpack. + } - try { - return HMCLModpackManager.readHMCLModpackManifest(file, charset); - } catch (Exception e) { - // ignore it, not a valid HMCL modpack. - } + try { + return HMCLModpackManager.readHMCLModpackManifest(zfs, charset); + } catch (Exception e) { + // ignore it, not a valid HMCL modpack. + } - try { - return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset); - } catch (Exception e) { - // ignore it, not a valid MultiMC modpack. - } + try { + return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset); + } catch (Exception e) { + // ignore it, not a valid MultiMC modpack. + } - try { - return ServerModpackManifest.readManifest(file, charset); - } catch (Exception e) { - // ignore it, not a valid Server modpack. + try { + return ServerModpackManifest.readManifest(zfs, charset); + } catch (Exception e) { + // ignore it, not a valid Server modpack. + } + + } catch (IOException ignored) { } try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java index 8b6fdc06c..c6cbfdb81 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.upgrade; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.util.Pack200Utils; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.tukaani.xz.XZInputStream; @@ -49,7 +50,7 @@ class HMCLDownloadTask extends FileDownloadTask { break; case PACK_XZ: - byte[] raw = Files.readAllBytes(target); + byte[] raw = FileUtils.readAllBytes(target); try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw)); JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) { Pack200Utils.unpack(in, out); 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 index a5547a1f3..4288a911a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -103,7 +103,7 @@ public class LibraryDownloadTask extends Task { else throw new LibraryDownloadException(library, t); } else { - if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath())); + if (xz) unpackLibrary(jar, FileUtils.readAllBytes(xzFile.toPath())); } } @@ -180,7 +180,7 @@ public class LibraryDownloadTask extends Task { if (checksums == null || checksums.isEmpty()) { return true; } - byte[] fileData = Files.readAllBytes(libPath.toPath()); + byte[] fileData = FileUtils.readAllBytes(libPath.toPath()); boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData))); if (!valid && libPath.getName().endsWith(".jar")) { valid = validateJar(fileData, checksums); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index cf8e712c1..46135e153 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -230,7 +230,7 @@ public final class OptiFineInstallTask extends Task { Path configClass = fs.getPath("Config.class"); if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class"); if (!Files.exists(configClass)) throw new IOException("Unrecognized installer"); - ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8); + ConstantPool pool = ConstantPoolScanner.parse(FileUtils.readAllBytes(configClass), ConstantType.UTF8); List constants = new ArrayList<>(); pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get())); String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java index ab9b5952c..c68699f05 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java @@ -104,13 +104,13 @@ public final class GameVersion { Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); if (Files.exists(minecraft)) { - Optional result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft)); + Optional result = getVersionOfClassMinecraft(FileUtils.readAllBytes(minecraft)); if (result.isPresent()) return result; } Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class"); if (Files.exists(minecraftServer)) - return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer)); + return getVersionFromClassMinecraftServer(FileUtils.readAllBytes(minecraftServer)); return Optional.empty(); } catch (IOException e) { return Optional.empty(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java index 57d0a230d..3fbd0b1bd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java @@ -23,13 +23,14 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.ZipFileSystem; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -121,11 +122,11 @@ public final class CurseManifest { * @throws JsonParseException if the manifest.json is missing or malformed. * @return the manifest. */ - public static Modpack readCurseForgeModpackManifest(Path zip, Charset encoding) throws IOException, JsonParseException { - String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding); + public static Modpack readCurseForgeModpackManifest(ZipFileSystem zip, Charset encoding) throws IOException, JsonParseException { + String json = FileUtils.readText(zip.getPath("manifest.json")); CurseManifest manifest = JsonUtils.fromNonNullJson(json, CurseManifest.class); return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), - CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest) { + Lang.ignoringException(() -> FileUtils.readText(zip.getPath("modlist.html")), "No description"), encoding, manifest) { @Override public Task getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) { return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java index 9b8e64ef3..914a68552 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java @@ -25,7 +25,6 @@ import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.*; -import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jetbrains.annotations.Nullable; @@ -33,7 +32,6 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -435,29 +433,26 @@ public class McbbsModpackManifest implements Validation { } private static Modpack fromManifestFile(Path manifestFile, Charset encoding) throws IOException, JsonParseException { - String json = FileUtils.readText(manifestFile, StandardCharsets.UTF_8); - McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class); + McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(FileUtils.readText(manifestFile), McbbsModpackManifest.class); return manifest.toModpack(encoding); } /** - * @param zip the MCBBS modpack file. + * @param fs the MCBBS modpack file. * @param encoding the modpack zip file encoding. - * @throws IOException if the file is not a valid zip file. - * @throws JsonParseException if the server-manifest.json is missing or malformed. * @return the manifest. + * @throws IOException if the file is not a valid zip file. + * @throws JsonParseException if the server-manifest.json is missing or malformed. */ - public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zip, encoding)) { - Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta"); - if (Files.exists(mcbbsPackMeta)) { - return fromManifestFile(mcbbsPackMeta, encoding); - } - Path manifestJson = fs.getPath("manifest.json"); - if (Files.exists(manifestJson)) { - return fromManifestFile(manifestJson, encoding); - } - throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found"); + public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException { + Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta"); + if (Files.exists(mcbbsPackMeta)) { + return fromManifestFile(mcbbsPackMeta, encoding); } + Path manifestJson = fs.getPath("manifest.json"); + if (Files.exists(manifestJson)) { + return fromManifestFile(manifestJson, encoding); + } + throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found"); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java index 502ff395d..20f78bbc2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 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 @@ -21,8 +21,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.ZipFileSystem; import java.io.File; import java.io.IOException; @@ -30,7 +30,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -348,24 +347,22 @@ public final class MultiMCInstanceConfiguration { } } - public static Modpack readMultiMCModpackManifest(Path modpackFile, Charset encoding) throws IOException { - try (FileSystem fs = CompressingUtils.readonly(modpackFile).setEncoding(encoding).build()) { - Path root = getRootPath(fs.getPath("/")); - MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root); - String name = FileUtils.getName(root, FileUtils.getNameWithoutExtension(modpackFile)); + public static Modpack readMultiMCModpackManifest(ZipFileSystem zipFile, Path filePath, Charset encoding) throws IOException { + Path root = getRootPath(zipFile.getPath("/")); + MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root); + String name = FileUtils.getNameWithoutExtension(filePath); - Path instancePath = root.resolve("instance.cfg"); - if (Files.notExists(instancePath)) - throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack."); - try (InputStream instanceStream = Files.newInputStream(instancePath)) { - MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest); - return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) { - @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) { - return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name); - } - }; - } + Path instancePath = root.resolve("instance.cfg"); + if (Files.notExists(instancePath)) + throw new IOException("`instance.cfg` not found, " + filePath + " is not a valid MultiMC modpack."); + try (InputStream instanceStream = Files.newInputStream(instancePath)) { + MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest); + return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) { + @Override + public Task getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) { + return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name); + } + }; } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java index 93a591a98..11a3b40e6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java @@ -32,6 +32,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.ZipFileSystem; import java.io.File; import java.io.IOException; @@ -144,11 +145,11 @@ public final class MultiMCModpackInstallTask extends Task { public void execute() throws Exception { Version version = repository.readVersionJson(name); - try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) { + try (ZipFileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) { Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/")); Path patches = root.resolve("patches"); - if (Files.exists(patches)) { + if (Files.isDirectory(patches)) { try (DirectoryStream directoryStream = Files.newDirectoryStream(patches)) { for (Path patchJson : directoryStream) { if (patchJson.toString().endsWith(".json")) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java index be9536904..6e636bf06 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java @@ -25,12 +25,12 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; -import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; -import java.nio.file.Path; +import java.nio.file.FileSystem; import java.util.Collections; import java.util.List; @@ -129,13 +129,13 @@ public class ServerModpackManifest implements Validation { } /** - * @param zip the CurseForge modpack file. - * @throws IOException if the file is not a valid zip file. - * @throws JsonParseException if the server-manifest.json is missing or malformed. + * @param fs the CurseForge modpack file. * @return the manifest. + * @throws IOException if the file is not a valid zip file. + * @throws JsonParseException if the server-manifest.json is missing or malformed. */ - public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException { - String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json", encoding); + public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException { + String json = FileUtils.readText(fs.getPath("server-manifest.json")); ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class); return manifest.toModpack(encoding); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java index b955afa92..d9777af65 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java @@ -42,6 +42,7 @@ import java.util.zip.ZipException; * @author huangyuhui */ public final class CompressingUtils { + private static final ZipFileSystemProvider MY_ZIPFS_PROVIDER = new ZipFileSystemProvider(); private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream() .filter(it -> "jar".equalsIgnoreCase(it.getScheme())) @@ -153,12 +154,12 @@ public final class CompressingUtils { return new Builder(zipFile, true).setUseTempFile(true); } - public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException { - return createReadOnlyZipFileSystem(zipFile, null); + public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException { + return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile()), true); } - public static FileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException { - return createZipFileSystem(zipFile, false, false, charset); + public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException { + return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile(), charset.name()), true); } public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException { @@ -199,7 +200,7 @@ public final class CompressingUtils { */ public static String readTextZipEntry(File zipFile, String name) throws IOException { try (ZipFile s = new ZipFile(zipFile)) { - return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8); + return readTextZipEntry(s, name); } } @@ -208,15 +209,20 @@ public final class CompressingUtils { * * @param zipFile the zip file * @param name the location of the text in zip file, something like A/B/C/D.txt + * @param encoding encoding of zip file. * @throws IOException if the file is not a valid zip file. * @return the plain text content of given file. */ public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException { try (ZipFile s = new ZipFile(zipFile.toFile(), encoding.name())) { - return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8); + return readTextZipEntry(s, name); } } + public static String readTextZipEntry(ZipFile s, String name) throws IOException { + return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8); + } + /** * Read the text content of a file in zip. * @@ -224,7 +230,7 @@ public final class CompressingUtils { * @param name the location of the text in zip file, something like A/B/C/D.txt * @return the plain text content of given file. */ - public static Optional readTextZipEntryQuietly(File file, String name) { + public static Optional readTextZipEntryQuietly(ZipFile file, String name) { try { return Optional.of(readTextZipEntry(file, name)); } catch (IOException e) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileSystemUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileSystemUtils.java new file mode 100644 index 000000000..95d6621a7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileSystemUtils.java @@ -0,0 +1,159 @@ +package org.jackhuang.hmcl.util.io; + +import java.util.regex.PatternSyntaxException; + +public final class FileSystemUtils { + private FileSystemUtils() { + } + + private static char EOL = 0; + + private static boolean isRegexMeta(char var0) { + return ".^$+{[]|()".indexOf(var0) != -1; + } + + private static boolean isGlobMeta(char var0) { + return "\\*?[{".indexOf(var0) != -1; + } + + private static char next(String var0, int var1) { + return var1 < var0.length() ? var0.charAt(var1) : EOL; + } + + public static String toRegexPattern(String var0) { + boolean var1 = false; + StringBuilder var2 = new StringBuilder("^"); + int var3 = 0; + + while(true) { + while(var3 < var0.length()) { + char var4 = var0.charAt(var3++); + switch(var4) { + case '*': + if (next(var0, var3) == '*') { + var2.append(".*"); + ++var3; + } else { + var2.append("[^/]*"); + } + break; + case ',': + if (var1) { + var2.append(")|(?:"); + } else { + var2.append(','); + } + break; + case '/': + var2.append(var4); + break; + case '?': + var2.append("[^/]"); + break; + case '[': + var2.append("[[^/]&&["); + if (next(var0, var3) == '^') { + var2.append("\\^"); + ++var3; + } else { + if (next(var0, var3) == '!') { + var2.append('^'); + ++var3; + } + + if (next(var0, var3) == '-') { + var2.append('-'); + ++var3; + } + } + + boolean var6 = false; + char var7 = 0; + + while(var3 < var0.length()) { + var4 = var0.charAt(var3++); + if (var4 == ']') { + break; + } + + if (var4 == '/') { + throw new PatternSyntaxException("Explicit 'name separator' in class", var0, var3 - 1); + } + + if (var4 == '\\' || var4 == '[' || var4 == '&' && next(var0, var3) == '&') { + var2.append('\\'); + } + + var2.append(var4); + if (var4 == '-') { + if (!var6) { + throw new PatternSyntaxException("Invalid range", var0, var3 - 1); + } + + if ((var4 = next(var0, var3++)) == EOL || var4 == ']') { + break; + } + + if (var4 < var7) { + throw new PatternSyntaxException("Invalid range", var0, var3 - 3); + } + + var2.append(var4); + var6 = false; + } else { + var6 = true; + var7 = var4; + } + } + + if (var4 != ']') { + throw new PatternSyntaxException("Missing ']", var0, var3 - 1); + } + + var2.append("]]"); + break; + case '\\': + if (var3 == var0.length()) { + throw new PatternSyntaxException("No character to escape", var0, var3 - 1); + } + + char var5 = var0.charAt(var3++); + if (isGlobMeta(var5) || isRegexMeta(var5)) { + var2.append('\\'); + } + + var2.append(var5); + break; + case '{': + if (var1) { + throw new PatternSyntaxException("Cannot nest groups", var0, var3 - 1); + } + + var2.append("(?:(?:"); + var1 = true; + break; + case '}': + if (var1) { + var2.append("))"); + var1 = false; + } else { + var2.append('}'); + } + break; + default: + if (isRegexMeta(var4)) { + var2.append('\\'); + } + + var2.append(var4); + } + } + + if (var1) { + throw new PatternSyntaxException("Missing '}", var0, var3 - 1); + } + + return var2.append('$').toString(); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index 920a7360d..5abb6ef6b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -113,11 +113,11 @@ public final class FileUtils { } public static String readText(File file) throws IOException { - return readText(file, UTF_8); + return readText(file.toPath()); } public static String readText(File file, Charset charset) throws IOException { - return new String(Files.readAllBytes(file.toPath()), charset); + return readText(file.toPath(), charset); } public static String readText(Path file) throws IOException { @@ -125,7 +125,11 @@ public final class FileUtils { } public static String readText(Path file, Charset charset) throws IOException { - return new String(Files.readAllBytes(file), charset); + return IOUtils.readFullyAsString(Files.newInputStream(file), charset); + } + + public static byte[] readAllBytes(Path file) throws IOException { + return IOUtils.readFullyAsByteArray(Files.newInputStream(file)); } /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java index a7b1e5ba3..7455f3286 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java @@ -101,7 +101,7 @@ public class Unzipper { */ public void unzip() throws IOException { Files.createDirectories(dest); - try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(encoding).setAutoDetectEncoding(true).build()) { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile, encoding)) { Path root = fs.getPath(subDirectory); if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/"))) throw new IllegalArgumentException("Subdirectory for unzipper must be absolute"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileAttributes.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileAttributes.java new file mode 100644 index 000000000..bd6966452 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileAttributes.java @@ -0,0 +1,81 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.util.io; + +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; + +public class ZipFileAttributes implements BasicFileAttributes { + + private final long size; + private final boolean symbolicLink; + private final boolean regularFile; + private final boolean directory; + + public ZipFileAttributes(long size, boolean symbolicLink, boolean regularFile, boolean directory) { + this.size = size; + this.symbolicLink = symbolicLink; + this.regularFile = regularFile; + this.directory = directory; + } + + @Override + public FileTime lastModifiedTime() { + return null; + } + + @Override + public FileTime lastAccessTime() { + return null; + } + + @Override + public FileTime creationTime() { + return null; + } + + @Override + public boolean isRegularFile() { + return regularFile; + } + + @Override + public boolean isDirectory() { + return directory; + } + + @Override + public boolean isSymbolicLink() { + return symbolicLink; + } + + @Override + public boolean isOther() { + return false; + } + + @Override + public long size() { + return size; + } + + @Override + public Object fileKey() { + return null; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystem.java new file mode 100644 index 000000000..fa85aa543 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystem.java @@ -0,0 +1,310 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.util.io; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.*; +import java.util.regex.Pattern; + +import static org.jackhuang.hmcl.util.Lang.toIterable; +import static org.jackhuang.hmcl.util.io.ZipPath.getPathComponents; + +public class ZipFileSystem extends FileSystem { + + private final ZipFileSystemProvider provider; + private final ZipFile zipFile; + private final boolean readOnly; + private final IndexNode root; + private final Map entries = new HashMap<>(); + final ZipPath rootDir; + + private volatile boolean isOpen = true; + + public ZipFileSystem(ZipFileSystemProvider provider, ZipFile zipFile, boolean readOnly) { + this.provider = provider; + this.zipFile = zipFile; + this.readOnly = readOnly; + + this.root = new IndexNode(null, true, ""); + this.rootDir = new ZipPath(this, "/"); + + buildTree(); + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + isOpen = false; + zipFile.close(); + } + + @Override + public boolean isOpen() { + return isOpen; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + @Override + public String getSeparator() { + return "/"; + } + + @Override + public Iterable getRootDirectories() { + return Collections.singleton(rootDir); + } + + @Override + public Iterable getFileStores() { + return null; + } + + @Override + public Set supportedFileAttributeViews() { + return null; + } + + @NotNull + @Override + public Path getPath(@NotNull String first, @NotNull String @NotNull ... more) { + StringBuilder sb = new StringBuilder(first); + for (String segment : more) { + if (segment.length() > 0) { + if (sb.length() > 0) { + sb.append('/'); + } + sb.append(segment); + } + } + return new ZipPath(this, sb.toString()); + } + + ZipFileAttributes readAttributes(ZipPath path) { + ensureOpen(); + + Optional inode = getInode(path); + if (!inode.isPresent()) return null; + return inode.get().getAttributes(); + } + + InputStream newInputStream(ZipPath path, OpenOption... options) throws IOException { + ensureOpen(); + + ZipPath realPath = path.toRealPath(); + ZipArchiveEntry entry = zipFile.getEntry(realPath.getEntryName()); + return zipFile.getInputStream(entry); + } + + DirectoryStream newDirectoryStream(ZipPath dir, DirectoryStream.Filter filter) throws IOException { + Optional inode = getInode(dir); + if (!inode.isPresent() || !inode.get().isDirectory()) throw new NotDirectoryException(dir.toString()); + + List list = new ArrayList<>(); + for (IndexNode child = inode.get().child; child != null; child = child.sibling) { + list.add(new ZipPath(this, child.name)); + } + + return new DirectoryStream() { + private volatile boolean isClosed = false; + private volatile Iterator itr; + + @Override + public synchronized Iterator iterator() { + if (isClosed) + throw new ClosedDirectoryStreamException(); + if (itr != null) + throw new IllegalStateException("Iterator has already been returned"); + itr = list.iterator(); + + return new Iterator() { + @Override + public boolean hasNext() { + if (isClosed) return false; + return itr.hasNext(); + } + + @Override + public Path next() { + if (isClosed) throw new NoSuchElementException(); + return itr.next(); + } + }; + } + + @Override + public synchronized void close() { + isClosed = true; + } + }; + } + + void checkAccess(ZipPath path) throws IOException { + ensureOpen(); + + if (!getInode(path.getEntryName()).isPresent()) { + throw new NoSuchFileException(path.toString()); + } + } + + private static final String GLOB_SYNTAX = "glob"; + private static final String REGEX_SYNTAX = "regex"; + + @Override + public PathMatcher getPathMatcher(String syntaxAndInput) { + int pos = syntaxAndInput.indexOf(':'); + if (pos <= 0 || pos == syntaxAndInput.length()) { + throw new IllegalArgumentException(); + } + String syntax = syntaxAndInput.substring(0, pos); + String input = syntaxAndInput.substring(pos + 1); + String expr; + if (syntax.equalsIgnoreCase(GLOB_SYNTAX)) { + expr = FileSystemUtils.toRegexPattern(input); + } else { + if (syntax.equalsIgnoreCase(REGEX_SYNTAX)) { + expr = input; + } else { + throw new UnsupportedOperationException("Syntax '" + syntax + + "' not recognized"); + } + } + // return matcher + final Pattern pattern = Pattern.compile(expr); + return path -> pattern.matcher(path.toString()).matches(); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + throw new UnsupportedOperationException(); + } + + @Override + public WatchService newWatchService() { + throw new UnsupportedOperationException(); + } + + private void ensureOpen() { + if (!isOpen) { + throw new ClosedFileSystemException(); + } + } + + private Optional getInode(String entryName) { + return Optional.ofNullable(entries.get(entryName)); + } + + private Optional getInode(ZipPath path) { + return getInode(path.toAbsolutePath().normalize().getEntryName()); + } + + private void buildTree() { + entries.put("", root); + + for (ZipArchiveEntry entry : toIterable(zipFile.getEntriesInPhysicalOrder())) { + List components = getPathComponents(entry.getName()); + + IndexNode node = new IndexNode(entry, entry.isDirectory(), String.join("/", components)); + entries.put(node.name, node); + while (true) { + if (components.size() == 0) break; + if (components.size() == 1) { + node.sibling = root.child; + root.child = node; + break; + } + + String parentName = String.join("/", components.subList(0, components.size() - 1)); + if (entries.containsKey(parentName)) { + IndexNode parent = entries.get(parentName); + node.sibling = parent.child; + parent.child = node; + break; + } + + // Add new pseudo directory entry + IndexNode parent = new IndexNode(null, true, parentName); + entries.put(parentName, parent); + node.sibling = parent.child; + parent.child = node; + node = parent; + } + } + } + + private class IndexNode { + private final ZipArchiveEntry entry; + private final boolean isDirectory; + private final String name; + + private ZipFileAttributes attributes; + + public IndexNode(ZipArchiveEntry entry, boolean isDirectory, String name) { + this.entry = entry; + this.isDirectory = isDirectory; + this.name = name; + } + + public boolean isDirectory() { + return isDirectory; + } + + public String getName() { + return name; + } + + public InputStream getInputStream() throws IOException { + if (entry == null) throw new IOException("Entry " + name + " cannot open"); + return zipFile.getInputStream(entry); + } + + public ZipFileAttributes getAttributes() { + if (attributes == null) { + if (entry == null) { + attributes = new ZipFileAttributes(0, false, false, true); + } else { + attributes = new ZipFileAttributes( + entry.getSize(), + entry.isUnixSymlink(), + !entry.isDirectory(), + entry.isDirectory() + ); + } + } + return attributes; + } + + IndexNode sibling; + IndexNode child; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystemProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystemProvider.java new file mode 100644 index 000000000..d9f348739 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipFileSystemProvider.java @@ -0,0 +1,139 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.util.io; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.Map; +import java.util.Set; + +import static org.jackhuang.hmcl.util.io.ZipPath.ensurePath; + +public class ZipFileSystemProvider extends FileSystemProvider { + @Override + public String getScheme() { + return "zip"; + } + + @Override + public ZipFileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public FileSystem getFileSystem(URI uri) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Path getPath(@NotNull URI uri) { + throw new UnsupportedOperationException(); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream newInputStream(Path path, OpenOption... options) throws IOException { + ZipPath zipPath = ensurePath(path); + return zipPath.getFileSystem().newInputStream(zipPath, options); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + ZipPath zipPath = ensurePath(dir); + return zipPath.getFileSystem().newDirectoryStream(zipPath, filter); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void delete(Path path) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return path.toRealPath().equals(path2.toRealPath()); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + return null; + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + ZipPath zipPath = ensurePath(path); + zipPath.checkAccess(modes); + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + return null; + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) { + //noinspection unchecked + return (A) ensurePath(path).getAttributes(); + } + return null; + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipPath.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipPath.java new file mode 100644 index 000000000..d5fbef584 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipPath.java @@ -0,0 +1,379 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.util.io; + +import org.jackhuang.hmcl.util.Lang; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.util.*; + +public class ZipPath implements Path { + + private final ZipFileSystem zfs; + private final List path; + private List normalized; + private final boolean absolute; + + ZipPath(ZipFileSystem zfs, String path) { + this(zfs, getPathComponents(path), path.startsWith("/")); + } + + ZipPath(ZipFileSystem zfs, List path, boolean absolute) { + this.zfs = zfs; + this.path = path; + this.absolute = absolute; + } + + @NotNull + @Override + public ZipFileSystem getFileSystem() { + return zfs; + } + + @Override + public boolean isAbsolute() { + return absolute; + } + + @Override + public ZipPath getRoot() { + if (this.isAbsolute()) + return zfs.rootDir; + else + return null; + } + + @Override + public ZipPath getFileName() { + if (path.size() <= 1) return this; + else return new ZipPath(zfs, Collections.singletonList(path.get(path.size() - 1)), false); + } + + @Override + public ZipPath getParent() { + if (path.isEmpty()) return null; + else if (path.size() == 1) return getRoot(); + else return new ZipPath(zfs, path.subList(0, path.size() - 1), absolute); + } + + @Override + public int getNameCount() { + return path.size(); + } + + @NotNull + @Override + public ZipPath getName(int index) { + if (index < 0 || index >= path.size()) throw new IllegalArgumentException(); + return new ZipPath(zfs, Collections.singletonList(path.get(index)), false); + } + + @NotNull + @Override + public ZipPath subpath(int beginIndex, int endIndex) { + if (beginIndex < 0 || beginIndex >= path.size() || endIndex > path.size() || beginIndex >= endIndex) { + throw new IllegalArgumentException(); + } + + return new ZipPath(zfs, path.subList(beginIndex, endIndex), absolute); + } + + @Override + public boolean startsWith(@NotNull Path other) { + ZipPath p1 = this; + ZipPath p2 = ensurePath(other); + if (p1.isAbsolute() != p2.isAbsolute() || p1.path.size() < p2.path.size()) { + return false; + } + int length = p2.path.size(); + for (int i = 0; i < length; i++) { + if (!Objects.equals(p1.path.get(i), p2.path.get(i))) { + return false; + } + } + return true; + } + + @Override + public boolean startsWith(@NotNull String other) { + return startsWith(getFileSystem().getPath(other)); + } + + @Override + public boolean endsWith(@NotNull Path other) { + ZipPath p1 = this; + ZipPath p2 = ensurePath(other); + + if (p2.isAbsolute() && !p1.isAbsolute() || + p2.isAbsolute() && p1.isAbsolute() && p1.path.size() != p2.path.size() || + p1.path.size() < p2.path.size() + ) { + return false; + } + + int length = p2.path.size(); + for (int i = 0; i < length; i++) { + if (!Objects.equals(p1.path.get(p1.path.size() - i - 1), p2.path.get(p2.path.size() - i - 1))) { + return false; + } + } + return true; + } + + @Override + public boolean endsWith(@NotNull String other) { + return endsWith(getFileSystem().getPath(other)); + } + + @NotNull + @Override + public ZipPath normalize() { + if (isNormalizable()) { + doNormalize(); + return new ZipPath(zfs, normalized, absolute); + } + return this; + } + + private boolean isNormalizable() { + for (String component : path) { + if (".".equals(component) || "..".equals(component)) { + return true; + } + } + return false; + } + + private void doNormalize() { + if (normalized != null) return; + Stack stack = new Stack<>(); + for (String component : path) { + if (".".equals(component)) { + continue; + } else if ("..".equals(component)) { + if (!stack.isEmpty()) stack.pop(); + } else { + stack.push(component); + } + } + normalized = new ArrayList<>(stack); + } + + @NotNull + @Override + public ZipPath resolve(@NotNull Path other) { + ZipPath p1 = this; + ZipPath p2 = ensurePath(other); + if (p2.isAbsolute()) return p2; + return new ZipPath(zfs, Lang.merge(p1.path, p2.path), absolute); + } + + @NotNull + @Override + public ZipPath resolve(@NotNull String other) { + return resolve(getFileSystem().getPath(other)); + } + + @NotNull + @Override + public ZipPath resolveSibling(@NotNull Path other) { + ZipPath parent = getParent(); + return parent == null ? ensurePath(other) : parent.resolve(other); + } + + @NotNull + @Override + public ZipPath resolveSibling(@NotNull String other) { + return resolveSibling(zfs.getPath(other)); + } + + @NotNull + @Override + public Path relativize(@NotNull Path other) { + ZipPath p1 = this; + ZipPath p2 = ensurePath(other); + + if (p2.equals(p1)) return new ZipPath(zfs, Collections.emptyList(), false); + if (p1.isAbsolute() != p2.isAbsolute()) throw new IllegalArgumentException(); + + int l = Math.min(p1.path.size(), p2.path.size()); + int common = 0; + while (common < l && Objects.equals(p1.path.get(common), p2.path.get(common))) common++; + int up = p1.path.size() - common; + List result = new ArrayList<>(); + for (int i = 0; i < up; i++) result.add(".."); + result.addAll(p2.path); + return new ZipPath(zfs, result, false); + } + + @NotNull + @Override + public URI toUri() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public ZipPath toAbsolutePath() { + if (isAbsolute()) { + return this; + } + + return new ZipPath(zfs, path, true); + } + + @NotNull + @Override + public ZipPath toRealPath(@NotNull LinkOption... options) throws IOException { + ZipPath absolute = toAbsolutePath().normalize(); + absolute.checkAccess(); + return absolute; + } + + void checkAccess(AccessMode... modes) throws IOException { + boolean w = false; + boolean x = false; + for (AccessMode mode : modes) { + switch (mode) { + case READ: + break; + case WRITE: + w = true; + break; + case EXECUTE: + x = true; + break; + default: + throw new UnsupportedOperationException(); + } + } + zfs.checkAccess(toAbsolutePath().normalize()); + if ((w && zfs.isReadOnly()) || x) { + throw new AccessDeniedException(toString()); + } + } + + @NotNull + @Override + public File toFile() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind @NotNull [] events, WatchEvent.Modifier... modifiers) throws IOException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind @NotNull ... events) throws IOException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Iterator iterator() { + return new Iterator() { + private int index = 0; + + public boolean hasNext() { + return index < getNameCount(); + } + + public Path next() { + if (index < getNameCount()) { + return getName(index++); + } + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ZipPath paths = (ZipPath) o; + return absolute == paths.absolute && path.equals(paths.path); + } + + @Override + public int hashCode() { + return Objects.hash(path, absolute); + } + + @Override + public int compareTo(@NotNull Path other) { + ZipPath p1 = this; + ZipPath p2 = ensurePath(other); + return p1.toString().compareTo(p2.toString()); + } + + ZipFileAttributes getAttributes() throws IOException { + ZipFileAttributes attributes = zfs.readAttributes(this); + if (attributes == null) throw new NoSuchFileException(toString()); + else return attributes; + } + + static List getPathComponents(String path) { + List components = new ArrayList<>(); + int lastSlash = 0; + for (int i = 0; i <= path.length(); i++) { + if (i == path.length() || path.charAt(i) == '/' || path.charAt(i) == '\\') { + if (i != lastSlash) { + String component = path.substring(lastSlash, i); + components.add(component); + } + + lastSlash = i + 1; + } + } + return components; + } + + private static String normalizePath(String path) { + return String.join("/", getPathComponents(path)); + } + + static ZipPath ensurePath(Path path) { + if (path == null) throw new NullPointerException(); + if (!(path instanceof ZipPath)) throw new ProviderMismatchException(); + return (ZipPath) path; + } + + String getEntryName() { + if (!isAbsolute()) throw new IllegalStateException(); + return String.join("/", path); + } + + @Override + public String toString() { + String str = String.join("/", path); + if (absolute) return "/" + str; + else return str; + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipFileSystemTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipFileSystemTest.java new file mode 100644 index 000000000..418483ee6 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipFileSystemTest.java @@ -0,0 +1,21 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.util.io; + +public class ZipFileSystemTest { +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipPathTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipPathTest.java new file mode 100644 index 000000000..16d9ea050 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/ZipPathTest.java @@ -0,0 +1,77 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.util.io; + +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.function.BiConsumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class ZipPathTest { + ZipFileSystemProvider provider = new ZipFileSystemProvider(); + ZipFileSystem zfs = new ZipFileSystem(provider, new ZipFile(new SeekableInMemoryByteChannel(IOUtils.readFullyAsByteArray(ZipPathTest.class.getResourceAsStream("/test.zip")))), true); + + public ZipPathTest() throws IOException { + + } + + private Path p(String path) { + return zfs.getPath(path); + } + + @Test + public void testNormalizePath() throws IOException { + BiConsumer equals = (expected, actual) -> { + assertEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize()); + }; + + BiConsumer notEquals = (expected, actual) -> { + assertNotEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize()); + }; + + equals.accept("/a/b/c/d", "/a\\b/c/d"); + equals.accept("/a/b/c/d", "/a\\b/c/d/"); + equals.accept("/a/b/c/d", "/a\\b/c//d"); + equals.accept("/a/b/c/d", "/a\\\\b/c/d"); + equals.accept("/a/b/c/d", "a/b/c/d"); + equals.accept("/a/b/c/d", "a/b/.c/../c/d"); + + notEquals.accept("/a/b/c/d", "/a\\b/c"); + } + + @Test + public void testRelativizePath() throws IOException { + assertEquals(p("../../a/b/c"), p("/a/b/c/a/b/c").relativize(p("/a/b/c/d/e"))); + + assertEquals(p("../.."), p("/a/b/c").relativize(p("/a/b/c/d/e"))); + assertEquals(p("../../"), p("/a/b/c").relativize(p("/a/b/c/d/e"))); + assertEquals(p("../../"), p("/a/b/c/").relativize(p("/a/b/c/d/e"))); + assertEquals(p("../.."), p("/a/b/c/").relativize(p("/a/b/c/d/e"))); + + assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c"))); + assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c"))); + assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c/"))); + assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c/"))); + } +}