import lombok.extern.slf4j.Slf4j; import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @Slf4j public final class ZipUtils { static final int MAX_FILES_NUM = 1024; // Max number of files private static final long THRESHOLD_SIZE = 100L * 1024L * 1024L; // 100MB private static final double THRESHOLD_RATIO = 10.0; private ZipUtils() { } public static void unzip(Path inputZipFile, Path outputDirectory) throws IOException { File destDir = new File(outputDirectory.toString()); try { // 先試試 utf8 unzip(inputZipFile, destDir, StandardCharsets.UTF_8); } catch (RuntimeException e) { log.error("UTF_8解壓縮失敗"); // 再試試看 big5 unzip(inputZipFile, destDir, Charset.forName("Big5")); } } private static void unzip(Path inputZipFile, File destDir, Charset charset) throws IOException { log.info("開始使用 Charset: {} 進行解壓縮", charset); long totalUncompressedSize = 0; int totalEntries = 0; try (ZipInputStream zis = new ZipInputStream(new FileInputStream(inputZipFile.toFile()), charset)) { ZipEntry zipEntry; while ((zipEntry = zis.getNextEntry()) != null) { byte[] buffer = new byte[1024]; File newFile = newFile(destDir, zipEntry); long uncompressedEntrySize = 0; if (zipEntry.isDirectory()) { if (!newFile.isDirectory() && !newFile.mkdirs()) { throw new IOException("Failed to create directory " + newFile); } } else { File parent = newFile.getParentFile(); if (!parent.isDirectory() && !parent.mkdirs()) { throw new IOException("Failed to create directory " + parent); } try (FileOutputStream fos = new FileOutputStream(newFile); BufferedOutputStream bos = new BufferedOutputStream(fos)) { int len; while ((len = zis.read(buffer)) > 0) { bos.write(buffer, 0, len); uncompressedEntrySize += len; totalUncompressedSize += len; checkSizeAndRatio(totalUncompressedSize, uncompressedEntrySize, zipEntry); } } } totalEntries++; checkNumberOfFiles(totalEntries); } } } private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException { File destFile = new File(destinationDir, zipEntry.getName()); String destDirPath = destinationDir.getCanonicalPath(); String destFilePath = destFile.getCanonicalPath(); if (!destFilePath.startsWith(destDirPath + File.separator)) { throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); } return destFile; } private static void checkSizeAndRatio(long totalUncompressedSize, long uncompressedEntrySize, ZipEntry zipEntry) { if (totalUncompressedSize > THRESHOLD_SIZE) { throw new IllegalStateException("Uncompressed size exceeds threshold."); } double compressionRatio = uncompressedEntrySize / (double) zipEntry.getCompressedSize(); if (compressionRatio > THRESHOLD_RATIO) { throw new IllegalStateException("Compression ratio exceeds threshold, possible zip bomb."); } } private static void checkNumberOfFiles(int totalEntries) { if (totalEntries > MAX_FILES_NUM) { throw new IllegalStateException("Too many files to unzip."); } } }