From 9969dc60c5278340b6b9a4d7facdde620e99d1f5 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 2 Aug 2025 19:33:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ArchiveFileTree=20(#4177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/util/io/CompressingUtils.java | 28 +++++-- .../hmcl/util/tree/ArchiveFileTree.java | 32 ++++++-- .../jackhuang/hmcl/util/tree/ZipFileTree.java | 18 +++-- .../hmcl/util/tree/ZipFileTreeTest.java | 76 +++++++++++++++++++ 4 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/tree/ZipFileTreeTest.java 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 b9ba5da3c..7ff6209f4 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 @@ -66,7 +66,7 @@ public final class CompressingUtils { cd.reset(); byte[] ba = entry.getRawName(); - int clen = (int)(ba.length * cd.maxCharsPerByte()); + int clen = (int) (ba.length * cd.maxCharsPerByte()); if (clen == 0) continue; if (clen <= cb.capacity()) cb.clear(); @@ -129,7 +129,19 @@ public final class CompressingUtils { } public static ZipArchiveReader openZipFile(Path zipFile) throws IOException { - return new ZipArchiveReader(Files.newByteChannel(zipFile)); + ZipArchiveReader zipReader = new ZipArchiveReader(Files.newByteChannel(zipFile)); + Charset suitableEncoding; + try { + suitableEncoding = findSuitableEncoding(zipReader); + if (suitableEncoding == StandardCharsets.UTF_8) + return zipReader; + } catch (Throwable e) { + IOUtils.closeQuietly(zipReader, e); + throw e; + } + + zipReader.close(); + return new ZipArchiveReader(Files.newByteChannel(zipFile), suitableEncoding); } public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException { @@ -221,9 +233,9 @@ public final class CompressingUtils { * Read the text content of a file in zip. * * @param zipFile the zip file - * @param name the location of the text in zip file, something like A/B/C/D.txt - * @throws IOException if the file is not a valid zip file. + * @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. + * @throws IOException if the file is not a valid zip file. */ public static String readTextZipEntry(File zipFile, String name) throws IOException { try (ZipArchiveReader s = new ZipArchiveReader(zipFile.toPath())) { @@ -235,9 +247,9 @@ public final class CompressingUtils { * Read the text content of a file in zip. * * @param zipFile the zip file - * @param name the location of the text in zip file, something like A/B/C/D.txt - * @throws IOException if the file is not a valid zip file. + * @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. + * @throws IOException if the file is not a valid zip file. */ public static String readTextZipEntry(ZipArchiveReader zipFile, String name) throws IOException { return IOUtils.readFullyAsString(zipFile.getInputStream(zipFile.getEntry(name))); @@ -247,9 +259,9 @@ public final class CompressingUtils { * Read the text content of a file in zip. * * @param zipFile the zip file - * @param name the location of the text in zip file, something like A/B/C/D.txt - * @throws IOException if the file is not a valid zip file. + * @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. + * @throws IOException if the file is not a valid zip file. */ public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException { try (ZipArchiveReader s = openZipFile(zipFile, encoding)) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java index 33a674c3f..8a0369e8a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java @@ -19,6 +19,9 @@ package org.jackhuang.hmcl.util.tree; import kala.compress.archivers.ArchiveEntry; import kala.compress.archivers.zip.ZipArchiveReader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; import java.io.Closeable; import java.io.IOException; @@ -48,7 +51,7 @@ public abstract class ArchiveFileTree implements Clos } protected final F file; - protected final Dir root = new Dir<>(); + protected final Dir root = new Dir<>(""); public ArchiveFileTree(F file) { this.file = file; @@ -62,7 +65,7 @@ public abstract class ArchiveFileTree implements Clos return root; } - public void addEntry(E entry) throws IOException { + protected void addEntry(E entry) throws IOException { String[] path = entry.getName().split("/"); Dir dir = root; @@ -78,7 +81,7 @@ public abstract class ArchiveFileTree implements Clos throw new IOException("A file and a directory have the same name: " + entry.getName()); } - dir = dir.subDirs.computeIfAbsent(item, name -> new Dir<>()); + dir = dir.subDirs.computeIfAbsent(item, Dir::new); } if (entry.isDirectory()) { @@ -113,16 +116,33 @@ public abstract class ArchiveFileTree implements Clos public abstract void close() throws IOException; public static final class Dir { - E entry; + private final String name; + private E entry; final Map> subDirs = new HashMap<>(); final Map files = new HashMap<>(); - public Map> getSubDirs() { + public Dir(String name) { + this.name = name; + } + + public boolean isRoot() { + return name.isEmpty(); + } + + public @NotNull String getName() { + return name; + } + + public @Nullable E getEntry() { + return entry; + } + + public @NotNull @UnmodifiableView Map> getSubDirs() { return subDirs; } - public Map getFiles() { + public @NotNull @UnmodifiableView Map getFiles() { return files; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java index ba8188362..602200279 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.tree; import kala.compress.archivers.zip.ZipArchiveEntry; import kala.compress.archivers.zip.ZipArchiveReader; +import org.jackhuang.hmcl.util.io.IOUtils; import java.io.IOException; import java.io.InputStream; @@ -27,25 +28,30 @@ import java.io.InputStream; * @author Glavo */ public final class ZipFileTree extends ArchiveFileTree { + private final boolean closeReader; + public ZipFileTree(ZipArchiveReader file) throws IOException { + this(file, true); + } + + public ZipFileTree(ZipArchiveReader file, boolean closeReader) throws IOException { super(file); + this.closeReader = closeReader; try { for (ZipArchiveEntry zipArchiveEntry : file.getEntries()) { addEntry(zipArchiveEntry); } } catch (Throwable e) { - try { - file.close(); - } catch (Throwable e2) { - e.addSuppressed(e2); - } + if (closeReader) + IOUtils.closeQuietly(file, e); throw e; } } @Override public void close() throws IOException { - file.close(); + if (closeReader) + file.close(); } @Override diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/tree/ZipFileTreeTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/tree/ZipFileTreeTest.java new file mode 100644 index 000000000..8aa2e0b0b --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/tree/ZipFileTreeTest.java @@ -0,0 +1,76 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.tree; + +import kala.compress.archivers.zip.ZipArchiveReader; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +public final class ZipFileTreeTest { + private static Path getTestFile(String name) { + try { + return Path.of(ZipFileTreeTest.class.getResource("/zip/" + name).toURI()); + } catch (URISyntaxException | NullPointerException e) { + throw new AssertionError("Resource not found: " + name, e); + } + } + + @Test + public void testClose() throws IOException { + Path testFile = getTestFile("utf-8.zip"); + + try (var channel = FileChannel.open(testFile, StandardOpenOption.READ)) { + var reader = new ZipArchiveReader(channel); + + try (var ignored = new ZipFileTree(reader, false)) { + } + + assertTrue(channel.isOpen()); + + try (var ignored = new ZipFileTree(reader)) { + } + + assertFalse(channel.isOpen()); + } + } + + @Test + public void test() throws IOException { + Path testFile = getTestFile("utf-8.zip"); + + try (var tree = new ZipFileTree(new ZipArchiveReader(testFile))) { + var root = tree.getRoot(); + assertEquals(2, root.getFiles().size()); + assertEquals(0, root.getSubDirs().size()); + + assertEquals("test.txt", root.getFiles().get("test.txt").getName()); + assertEquals("中文.txt", root.getFiles().get("中文.txt").getName()); + assertNull(root.getFiles().get("other.txt")); + } + } +}