Skip to content

Commit 02d1ba4

Browse files
Merge pull request #93 from Codeforces/dev-mikemirzayanov
ZipUtil#unzip switched from net.lingala.zip4j to ZipInputStream
2 parents a4b52dd + 8965d83 commit 02d1ba4

File tree

1 file changed

+103
-0
lines changed
  • code/src/main/java/com/codeforces/commons/compress

1 file changed

+103
-0
lines changed

code/src/main/java/com/codeforces/commons/compress/ZipUtil.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.commons.io.IOUtils;
2323
import org.apache.commons.io.filefilter.NameFileFilter;
2424
import org.apache.commons.lang3.mutable.MutableBoolean;
25+
import org.apache.log4j.Logger;
2526
import org.jetbrains.annotations.Contract;
2627

2728
import javax.annotation.Nonnull;
@@ -30,6 +31,7 @@
3031
import java.nio.charset.Charset;
3132
import java.nio.charset.StandardCharsets;
3233
import java.nio.file.Files;
34+
import java.nio.file.Path;
3335
import java.util.ArrayList;
3436
import java.util.Comparator;
3537
import java.util.List;
@@ -40,6 +42,8 @@
4042
*/
4143
@SuppressWarnings({"WeakerAccess", "unused"})
4244
public final class ZipUtil {
45+
private static final Logger logger = Logger.getLogger(ZipUtil.class);
46+
4347
@SuppressWarnings("unused")
4448
public static final int MINIMAL_COMPRESSION_LEVEL = 0;
4549
public static final int DEFAULT_COMPRESSION_LEVEL = 5;
@@ -261,8 +265,107 @@ public static void unzip(File zipArchive, File destinationDirectory) throws IOEx
261265
unzip(zipArchive, destinationDirectory, null);
262266
}
263267

268+
/**
269+
* Unzips a ZIP-archive to the specified directory.
270+
*
271+
* @param zipArchive ZIP-archive to unzip
272+
* @param destinationDirectory directory to unzip to
273+
* @throws IOException if any I/O-exception occurred
274+
*/
264275
public static void unzip(File zipArchive, File destinationDirectory, @Nullable FileFilter skipFilter)
265276
throws IOException {
277+
long startTimeMillis = System.currentTimeMillis();
278+
long compressedSize = zipArchive.length();
279+
long totalUncompressedSize = 0L;
280+
281+
FileUtil.ensureDirectoryExists(destinationDirectory);
282+
Path destPath = destinationDirectory.toPath().toRealPath();
283+
284+
int count = 0;
285+
286+
try (ZipInputStream zis = new ZipInputStream(
287+
new BufferedInputStream(Files.newInputStream(zipArchive.toPath())))) {
288+
ZipEntry entry;
289+
290+
while ((entry = zis.getNextEntry()) != null && count < MAX_ZIP_ENTRY_COUNT) {
291+
try {
292+
String entryName = entry.getName().replace('\\', '/');
293+
294+
File targetFile = new File(destinationDirectory, entryName).getCanonicalFile();
295+
if (!targetFile.getAbsolutePath().startsWith(destPath.toString())) {
296+
throw new IOException("ZIP entry tries to escape destination directory: " + entryName);
297+
}
298+
299+
if (skipFilter != null && skipFilter.accept(targetFile)) {
300+
continue; // Entry will be closed in finally block
301+
}
302+
303+
if (entry.isDirectory()) {
304+
FileUtil.ensureDirectoryExists(targetFile);
305+
} else {
306+
// Check size if known upfront
307+
long size = entry.getSize();
308+
if (size > MAX_ZIP_ENTRY_SIZE) {
309+
throw new IOException(String.format("Entry '%s' (%s) is larger than %s.",
310+
entryName, FileUtil.formatSize(size),
311+
FileUtil.formatSize(MAX_ZIP_ENTRY_SIZE)));
312+
}
313+
314+
// Ensure parent dirs exist
315+
File parent = targetFile.getParentFile();
316+
if (!parent.exists() && !parent.mkdirs()) {
317+
throw new IOException("Failed to create parent directory: " + parent);
318+
}
319+
320+
Files.deleteIfExists(targetFile.toPath());
321+
Path targetPath = targetFile.toPath();
322+
323+
try (OutputStream out = new BufferedOutputStream(
324+
Files.newOutputStream(targetPath))) {
325+
byte[] buffer = new byte[65536]; // 64 KiB buffer
326+
int read;
327+
long totalRead = 0;
328+
329+
while ((read = zis.read(buffer)) != -1) {
330+
totalRead += read;
331+
if (totalRead > MAX_ZIP_ENTRY_SIZE) {
332+
throw new IOException("Extracted data exceeds allowed size for: " + entryName);
333+
}
334+
out.write(buffer, 0, read);
335+
}
336+
337+
totalUncompressedSize += totalRead;
338+
} catch (IOException e) {
339+
// Clean up partially created file on error
340+
Files.deleteIfExists(targetPath);
341+
throw e;
342+
}
343+
}
344+
345+
++count;
346+
} finally {
347+
// Always close the current entry, even on exceptions
348+
try {
349+
zis.closeEntry();
350+
} catch (IOException e) {
351+
// Log warning but don't mask original exception
352+
// logger.warn("Failed to close ZIP entry", e);
353+
}
354+
}
355+
}
356+
}
357+
358+
String message = String.format(
359+
"Unzipped %d entries from '%s' to '%s' in %d ms. Compressed size: %s, Uncompressed size: %s.",
360+
count, zipArchive.getAbsolutePath(), destinationDirectory.getAbsolutePath(),
361+
System.currentTimeMillis() - startTimeMillis,
362+
FileUtil.formatSize(compressedSize), FileUtil.formatSize(totalUncompressedSize)
363+
);
364+
logger.info(message);
365+
}
366+
367+
public static void unzip2(File zipArchive, File destinationDirectory, @Nullable FileFilter skipFilter)
368+
throws IOException {
266369
try (net.lingala.zip4j.ZipFile zipFile = new net.lingala.zip4j.ZipFile(zipArchive)) {
267370
FileUtil.ensureDirectoryExists(destinationDirectory);
268371

0 commit comments

Comments
 (0)