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

View File

@@ -73,22 +73,22 @@ public class ModpackInstallTask<T> extends Task<Void> {
.setTerminateIfSubDirectoryNotExists()
.setReplaceExistentFile(true)
.setEncoding(charset)
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> {
if (isDirectory) return true;
if (!callback.test(entryPath)) return false;
entries.add(entryPath);
.setFilter((zipEntry, destFile, relativePath) -> {
if (zipEntry.isDirectory()) return true;
if (!callback.test(relativePath)) return false;
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.
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.
return false;
} else {
// 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.
String fileHash = DigestUtils.digestToString("SHA-1", destPath);
String oldHash = files.get(entryPath).getHash();
String fileHash = DigestUtils.digestToString("SHA-1", destFile);
String oldHash = files.get(relativePath).getHash();
return Objects.equals(oldHash, fileHash);
}
}).unzip();

View File

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