mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-03-19 17:50:26 +08:00
feat: custom zipfilesystem.
This commit is contained in:
parent
ec37b32eaa
commit
9706a799be
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
|
@ -103,7 +103,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
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<Void> {
|
||||
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);
|
||||
|
@ -230,7 +230,7 @@ public final class OptiFineInstallTask extends Task<Version> {
|
||||
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<String> constants = new ArrayList<>();
|
||||
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
|
||||
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
|
||||
|
@ -104,13 +104,13 @@ public final class GameVersion {
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
Optional<String> 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();
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Void> {
|
||||
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<Path> directoryStream = Files.newDirectoryStream(patches)) {
|
||||
for (Path patchJson : directoryStream) {
|
||||
if (patchJson.toString().endsWith(".json")) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<String> readTextZipEntryQuietly(File file, String name) {
|
||||
public static Optional<String> readTextZipEntryQuietly(ZipFile file, String name) {
|
||||
try {
|
||||
return Optional.of(readTextZipEntry(file, name));
|
||||
} catch (IOException e) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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");
|
||||
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String, IndexNode> 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<Path> getRootDirectories() {
|
||||
return Collections.singleton(rootDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<FileStore> getFileStores() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> 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<IndexNode> 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<Path> newDirectoryStream(ZipPath dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
|
||||
Optional<IndexNode> inode = getInode(dir);
|
||||
if (!inode.isPresent() || !inode.get().isDirectory()) throw new NotDirectoryException(dir.toString());
|
||||
|
||||
List<ZipPath> list = new ArrayList<>();
|
||||
for (IndexNode child = inode.get().child; child != null; child = child.sibling) {
|
||||
list.add(new ZipPath(this, child.name));
|
||||
}
|
||||
|
||||
return new DirectoryStream<Path>() {
|
||||
private volatile boolean isClosed = false;
|
||||
private volatile Iterator<ZipPath> itr;
|
||||
|
||||
@Override
|
||||
public synchronized Iterator<Path> iterator() {
|
||||
if (isClosed)
|
||||
throw new ClosedDirectoryStreamException();
|
||||
if (itr != null)
|
||||
throw new IllegalStateException("Iterator has already been returned");
|
||||
itr = list.iterator();
|
||||
|
||||
return new Iterator<Path>() {
|
||||
@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<IndexNode> getInode(String entryName) {
|
||||
return Optional.ofNullable(entries.get(entryName));
|
||||
}
|
||||
|
||||
private Optional<IndexNode> getInode(ZipPath path) {
|
||||
return getInode(path.toAbsolutePath().normalize().getEntryName());
|
||||
}
|
||||
|
||||
private void buildTree() {
|
||||
entries.put("", root);
|
||||
|
||||
for (ZipArchiveEntry entry : toIterable(zipFile.getEntriesInPhysicalOrder())) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String, ?> 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<? extends OpenOption> 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<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> 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 extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
|
||||
if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) {
|
||||
//noinspection unchecked
|
||||
return (A) ensurePath(path).getAttributes();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> 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();
|
||||
}
|
||||
}
|
379
HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipPath.java
Normal file
379
HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/ZipPath.java
Normal file
@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String> path;
|
||||
private List<String> normalized;
|
||||
private final boolean absolute;
|
||||
|
||||
ZipPath(ZipFileSystem zfs, String path) {
|
||||
this(zfs, getPathComponents(path), path.startsWith("/"));
|
||||
}
|
||||
|
||||
ZipPath(ZipFileSystem zfs, List<String> 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<String> 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<String> 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<Path> iterator() {
|
||||
return new Iterator<Path>() {
|
||||
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<String> getPathComponents(String path) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
public class ZipFileSystemTest {
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String, String> equals = (expected, actual) -> {
|
||||
assertEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize());
|
||||
};
|
||||
|
||||
BiConsumer<String, String> 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/")));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user