将 Unzipper 迁移至 kala-compress (#5148)

This commit is contained in:
Glavo
2026-01-06 21:19:21 +08:00
committed by GitHub
parent f1bd4bffb7
commit 686f59e21b
3 changed files with 129 additions and 75 deletions

View File

@@ -397,9 +397,12 @@ public class DefaultLauncher extends Launcher {
for (Library library : version.getLibraries()) for (Library library : version.getLibraries())
if (library.isNative()) if (library.isNative())
new Unzipper(repository.getLibraryFile(version, library), destination) new Unzipper(repository.getLibraryFile(version, library), destination)
.setFilter((zipEntry, isDirectory, destFile, path) -> { .setFilter((zipEntry, destFile, relativePath) -> {
if (!isDirectory && Files.isRegularFile(destFile) && Files.size(destFile) == Files.size(zipEntry)) if (!zipEntry.isDirectory() && !zipEntry.isUnixSymlink()
&& Files.isRegularFile(destFile)
&& zipEntry.getSize() == Files.size(destFile)) {
return false; return false;
}
String ext = FileUtils.getExtension(destFile); String ext = FileUtils.getExtension(destFile);
if (ext.equals("sha1") || ext.equals("git")) if (ext.equals("sha1") || ext.equals("git"))
return false; return false;
@@ -411,7 +414,7 @@ public class DefaultLauncher extends Launcher {
return false; return false;
} }
return library.getExtract().shouldExtract(path); return library.getExtract().shouldExtract(relativePath);
}) })
.setReplaceExistentFile(false).unzip(); .setReplaceExistentFile(false).unzip();
} catch (IOException e) { } catch (IOException e) {

View File

@@ -73,22 +73,22 @@ public class ModpackInstallTask<T> extends Task<Void> {
.setTerminateIfSubDirectoryNotExists() .setTerminateIfSubDirectoryNotExists()
.setReplaceExistentFile(true) .setReplaceExistentFile(true)
.setEncoding(charset) .setEncoding(charset)
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> { .setFilter((zipEntry, destFile, relativePath) -> {
if (isDirectory) return true; if (zipEntry.isDirectory()) return true;
if (!callback.test(entryPath)) return false; if (!callback.test(relativePath)) return false;
entries.add(entryPath); entries.add(relativePath);
if (!files.containsKey(entryPath)) { if (!files.containsKey(relativePath)) {
// If old modpack does not have this entry, add this entry or override the file that user added. // If old modpack does not have this entry, add this entry or override the file that user added.
return true; return true;
} else if (!Files.exists(destPath)) { } else if (!Files.exists(destFile)) {
// If both old and new modpacks have this entry, but the file is deleted by user, leave it missing. // If both old and new modpacks have this entry, but the file is deleted by user, leave it missing.
return false; return false;
} else { } else {
// If both old and new modpacks have this entry, and user has modified this file, // If both old and new modpacks have this entry, and user has modified this file,
// we will not replace it since this modified file is what user expects. // we will not replace it since this modified file is what user expects.
String fileHash = DigestUtils.digestToString("SHA-1", destPath); String fileHash = DigestUtils.digestToString("SHA-1", destFile);
String oldHash = files.get(entryPath).getHash(); String oldHash = files.get(relativePath).getHash();
return Objects.equals(oldHash, fileHash); return Objects.equals(oldHash, fileHash);
} }
}).unzip(); }).unzip();

View File

@@ -17,57 +17,54 @@
*/ */
package org.jackhuang.hmcl.util.io; package org.jackhuang.hmcl.util.io;
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public final class Unzipper { public final class Unzipper {
private final Path zipFile, dest; private final Path zipFile, dest;
private boolean replaceExistentFile = false; private boolean replaceExistentFile = false;
private boolean terminateIfSubDirectoryNotExists = false; private boolean terminateIfSubDirectoryNotExists = false;
private String subDirectory = "/"; private String subDirectory = "/";
private FileFilter filter = null; private EntryFilter filter;
private Charset encoding = StandardCharsets.UTF_8; private Charset encoding = StandardCharsets.UTF_8;
/** /// Decompress the given zip file to a directory.
* Decompress the given zip file to a directory. ///
* /// @param zipFile the input zip file to be uncompressed
* @param zipFile the input zip file to be uncompressed /// @param destDir the dest directory to hold uncompressed files
* @param destDir the dest directory to hold uncompressed files
*/
public Unzipper(Path zipFile, Path destDir) { public Unzipper(Path zipFile, Path destDir) {
this.zipFile = zipFile; this.zipFile = zipFile;
this.dest = destDir; this.dest = destDir;
} }
/** /// True if replace the existent files in destination directory,
* True if replace the existent files in destination directory, /// otherwise those conflict files will be ignored.
* otherwise those conflict files will be ignored.
*/
public Unzipper setReplaceExistentFile(boolean replaceExistentFile) { public Unzipper setReplaceExistentFile(boolean replaceExistentFile) {
this.replaceExistentFile = replaceExistentFile; this.replaceExistentFile = replaceExistentFile;
return this; return this;
} }
/** /// Will be called for every entry in the zip file.
* Will be called for every entry in the zip file. /// Callback returns false if you want leave the specific file uncompressed.
* Callback returns false if you want leave the specific file uncompressed. public Unzipper setFilter(EntryFilter filter) {
*/
public Unzipper setFilter(FileFilter filter) {
this.filter = filter; this.filter = filter;
return this; return this;
} }
/** /// Will only uncompress files in the "subDirectory", their path will be also affected.
* Will only uncompress files in the "subDirectory", their path will be also affected. ///
* /// For example, if you set subDirectory to /META-INF, files in /META-INF/ will be
* For example, if you set subDirectory to /META-INF, files in /META-INF/ will be /// uncompressed to the destination directory without creating META-INF folder.
* uncompressed to the destination directory without creating META-INF folder. ///
* /// Default value: "/"
* Default value: "/"
*/
public Unzipper setSubDirectory(String subDirectory) { public Unzipper setSubDirectory(String subDirectory) {
this.subDirectory = FileUtils.normalizePath(subDirectory); this.subDirectory = FileUtils.normalizePath(subDirectory);
return this; return this;
@@ -83,53 +80,107 @@ public final class Unzipper {
return this; return this;
} }
/** private ZipArchiveReader openReader() throws IOException {
* Decompress the given zip file to a directory. ZipArchiveReader zipReader = new ZipArchiveReader(Files.newByteChannel(zipFile));
*
* @throws IOException if zip file is malformed or filesystem error. Charset suitableEncoding;
*/ try {
if (encoding != StandardCharsets.UTF_8 && CompressingUtils.testEncoding(zipReader, encoding)) {
suitableEncoding = encoding;
} else {
suitableEncoding = CompressingUtils.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);
}
/// Decompress the given zip file to a directory.
///
/// @throws IOException if zip file is malformed or filesystem error.
public void unzip() throws IOException { public void unzip() throws IOException {
Files.createDirectories(dest); Path destDir = this.dest.toAbsolutePath().normalize();
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(encoding).setAutoDetectEncoding(true).build()) { Files.createDirectories(destDir);
Path root = fs.getPath(subDirectory);
if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/")))
throw new IllegalArgumentException("Subdirectory for unzipper must be absolute");
if (terminateIfSubDirectoryNotExists && Files.notExists(root)) CopyOption[] copyOptions = replaceExistentFile
return; ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING}
: new CopyOption[]{};
Files.walkFileTree(root, new SimpleFileVisitor<Path>() { long entryCount = 0L;
@Override try (ZipArchiveReader reader = openReader()) {
public FileVisitResult visitFile(Path file, String pathPrefix = StringUtils.addSuffix(subDirectory, "/");
BasicFileAttributes attrs) throws IOException {
String relativePath = root.relativize(file).toString(); for (ZipArchiveEntry entry : reader.getEntries()) {
Path destFile = dest.resolve(relativePath); String normalizedPath = FileUtils.normalizePath(entry.getName());
if (filter != null && !filter.accept(file, false, destFile, relativePath)) if (!normalizedPath.startsWith(pathPrefix)) {
return FileVisitResult.CONTINUE; continue;
try { }
Files.copy(file, destFile, replaceExistentFile ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{});
} catch (FileAlreadyExistsException e) { String relativePath = normalizedPath.substring(pathPrefix.length());
Path destFile = destDir.resolve(relativePath).toAbsolutePath().normalize();
if (!destFile.startsWith(destDir)) {
throw new IOException("Zip entry is trying to write outside of the destination directory: " + entry.getName());
}
if (filter != null && !filter.accept(entry, destFile, relativePath)) {
continue;
}
entryCount++;
if (entry.isDirectory()) {
Files.createDirectories(destFile);
} else {
Files.createDirectories(destFile.getParent());
if (entry.isUnixSymlink()) {
String linkTarget = reader.getUnixSymlink(entry);
if (replaceExistentFile) if (replaceExistentFile)
throw e; Files.deleteIfExists(destFile);
}
return FileVisitResult.CONTINUE;
}
@Override Path targetPath;
public FileVisitResult preVisitDirectory(Path dir, try {
BasicFileAttributes attrs) throws IOException { targetPath = Path.of(linkTarget);
String relativePath = root.relativize(dir).toString(); } catch (InvalidPathException e) {
Path dirToCreate = dest.resolve(relativePath); throw new IOException("Zip entry has an invalid symlink target: " + entry.getName(), e);
if (filter != null && !filter.accept(dir, true, dirToCreate, relativePath)) }
return FileVisitResult.SKIP_SUBTREE;
Files.createDirectories(dirToCreate); if (!destFile.getParent().resolve(targetPath).toAbsolutePath().normalize().startsWith(destDir)) {
return FileVisitResult.CONTINUE; throw new IOException("Zip entry is trying to create a symlink outside of the destination directory: " + entry.getName());
}
try {
Files.createSymbolicLink(destFile, targetPath);
} catch (FileAlreadyExistsException ignored) {
}
} else {
try (InputStream input = reader.getInputStream(entry)) {
Files.copy(input, destFile, copyOptions);
} catch (FileAlreadyExistsException e) {
if (replaceExistentFile)
throw e;
}
if (entry.getUnixMode() != 0 && OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
Files.setPosixFilePermissions(destFile, FileUtils.parsePosixFilePermission(entry.getUnixMode()));
}
}
} }
}); }
if (entryCount == 0 && !terminateIfSubDirectoryNotExists) {
throw new NoSuchFileException("Subdirectory " + subDirectory + " does not exist in the zip file.");
}
} }
} }
public interface FileFilter { @FunctionalInterface
boolean accept(Path zipEntry, boolean isDirectory, Path destFile, String entryPath) throws IOException; public interface EntryFilter {
boolean accept(ZipArchiveEntry zipArchiveEntry, Path destFile, String relativePath) throws IOException;
} }
} }