8347712: IllegalStateException on multithreaded ZipFile access with non-UTF8 charset

8355975: ZipFile uses incorrect Charset if another instance for the same ZIP file was constructed with a different Charset

Co-authored-by: Eirik Bjørsnøs <eirbjo@openjdk.org>
Reviewed-by: eirbjo, lancea, redestad, alanb
This commit is contained in:
Jaikiran Pai 2025-05-14 01:53:19 +00:00
parent 530d14a16e
commit 2c4e8d211a
4 changed files with 365 additions and 56 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -38,7 +38,10 @@ import jdk.internal.util.ArraysSupport;
import sun.nio.cs.UTF_8;
/**
* Utility class for ZIP file entry name and comment decoding and encoding
* Utility class for ZIP file entry name and comment decoding and encoding.
* <p>
* The {@code ZipCoder} for UTF-8 charset is thread safe, {@code ZipCoder}
* for other charsets require external synchronization.
*/
class ZipCoder {
@ -174,6 +177,13 @@ class ZipCoder {
return dec;
}
/**
* {@return the {@link Charset} used by this {@code ZipCoder}}
*/
final Charset charset() {
return this.cs;
}
private CharsetEncoder encoder() {
if (enc == null) {
enc = cs.newEncoder()

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1995, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -82,6 +82,8 @@ public class ZipFile implements ZipConstants, Closeable {
private final String filePath; // ZIP file path
private final String fileName; // name of the file
// Used when decoding entry names and comments
private final ZipCoder zipCoder;
private volatile boolean closeRequested;
// The "resource" used by this ZIP file that needs to be
@ -198,7 +200,8 @@ public class ZipFile implements ZipConstants, Closeable {
this.fileName = file.getName();
long t0 = System.nanoTime();
this.res = new CleanableResource(this, ZipCoder.get(charset), file, mode);
this.zipCoder = ZipCoder.get(charset);
this.res = new CleanableResource(this, zipCoder, file, mode);
PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0);
PerfCounter.getZipFileCount().increment();
@ -265,7 +268,7 @@ public class ZipFile implements ZipConstants, Closeable {
// If there is a problem decoding the byte array which represents
// the ZIP file comment, return null;
try {
return res.zsrc.zc.toString(res.zsrc.comment);
return zipCoder.toString(res.zsrc.comment);
} catch (IllegalArgumentException iae) {
return null;
}
@ -288,7 +291,7 @@ public class ZipFile implements ZipConstants, Closeable {
// Look up the name and CEN header position of the entry.
// The resolved name may include a trailing slash.
// See Source::getEntryPos for details.
EntryPos pos = res.zsrc.getEntryPos(name, true);
EntryPos pos = res.zsrc.getEntryPos(name, true, zipCoder);
if (pos != null) {
entry = getZipEntry(pos.name, pos.pos);
}
@ -328,7 +331,7 @@ public class ZipFile implements ZipConstants, Closeable {
if (Objects.equals(lastEntryName, entry.name)) {
pos = lastEntryPos;
} else {
EntryPos entryPos = zsrc.getEntryPos(entry.name, false);
EntryPos entryPos = zsrc.getEntryPos(entry.name, false, zipCoder);
if (entryPos != null) {
pos = entryPos.pos;
} else {
@ -363,6 +366,35 @@ public class ZipFile implements ZipConstants, Closeable {
}
}
/**
* Determines and returns a {@link ZipCoder} to use for decoding
* name and comment fields of the ZIP entry identified by the {@code pos}
* in the ZIP file's {@code cen}.
* <p>
* A ZIP entry's name and comment fields may be encoded using UTF-8, in
* which case this method returns a UTF-8 capable {@code ZipCoder}. If the
* entry doesn't require UTF-8, then this method returns the {@code fallback}
* {@code ZipCoder}.
*
* @param cen the CEN
* @param pos the ZIP entry's position in CEN
* @param fallback the fallback ZipCoder to return if the entry doesn't require UTF-8
*/
private static ZipCoder zipCoderFor(final byte[] cen, final int pos, final ZipCoder fallback) {
if (fallback.isUTF8()) {
// the fallback ZipCoder is capable of handling UTF-8,
// so no need to parse the entry flags to determine if
// the entry has UTF-8 flag.
return fallback;
}
if ((CENFLG(cen, pos) & USE_UTF8) != 0) {
// entry requires a UTF-8 ZipCoder
return ZipCoder.UTF8;
}
// entry doesn't require a UTF-8 ZipCoder
return fallback;
}
private static class InflaterCleanupAction implements Runnable {
private final Inflater inf;
private final CleanableResource res;
@ -561,7 +593,7 @@ public class ZipFile implements ZipConstants, Closeable {
private String getEntryName(int pos) {
byte[] cen = res.zsrc.cen;
int nlen = CENNAM(cen, pos);
ZipCoder zc = res.zsrc.zipCoderForPos(pos);
ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
return zc.toString(cen, pos + CENHDR, nlen);
}
@ -632,7 +664,7 @@ public class ZipFile implements ZipConstants, Closeable {
}
if (clen != 0) {
int start = pos + CENHDR + nlen + elen;
ZipCoder zc = res.zsrc.zipCoderForPos(pos);
ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
e.comment = zc.toString(cen, start, clen);
}
lastEntryName = e.name;
@ -664,11 +696,12 @@ public class ZipFile implements ZipConstants, Closeable {
Source zsrc;
CleanableResource(ZipFile zf, ZipCoder zc, File file, int mode) throws IOException {
CleanableResource(ZipFile zf, ZipCoder zipCoder, File file, int mode) throws IOException {
assert zipCoder != null : "null ZipCoder";
this.cleanable = CleanerFactory.cleaner().register(zf, this);
this.istreams = Collections.newSetFromMap(new WeakHashMap<>());
this.inflaterCache = new ArrayDeque<>();
this.zsrc = Source.get(file, (mode & OPEN_DELETE) != 0, zc);
this.zsrc = Source.get(file, (mode & OPEN_DELETE) != 0, zipCoder);
}
void clean() {
@ -1109,6 +1142,7 @@ public class ZipFile implements ZipConstants, Closeable {
// Represents the resolved name and position of a CEN record
static record EntryPos(String name, int pos) {}
// Implementation note: This class is thread safe.
private static class Source {
// While this is only used from ZipFile, defining it there would cause
// a bootstrap cycle that would leave this initialized as null
@ -1121,7 +1155,6 @@ public class ZipFile implements ZipConstants, Closeable {
private static final int MAX_CEN_SIZE = ArraysSupport.SOFT_MAX_ARRAY_LENGTH;
private final Key key; // the key in files
private final @Stable ZipCoder zc; // ZIP coder used to decode/encode
private int refs = 1;
@ -1157,8 +1190,9 @@ public class ZipFile implements ZipConstants, Closeable {
private int[] entries; // array of hashed cen entry
// Checks the entry at offset pos in the CEN, calculates the Entry values as per above,
// then returns the length of the entry name.
private int checkAndAddEntry(int pos, int index)
// then returns the length of the entry name. Uses the given zipCoder for processing the
// entry name and the entry comment (if any).
private int checkAndAddEntry(final int pos, final int index, final ZipCoder zipCoder)
throws ZipException
{
byte[] cen = this.cen;
@ -1196,21 +1230,20 @@ public class ZipFile implements ZipConstants, Closeable {
}
try {
ZipCoder zcp = zipCoderForPos(pos);
int hash = zcp.checkedHash(cen, entryPos, nlen);
int hash = zipCoder.checkedHash(cen, entryPos, nlen);
int hsh = (hash & 0x7fffffff) % tablelen;
int next = table[hsh];
table[hsh] = index;
// Record the CEN offset and the name hash in our hash cell.
entries[index++] = hash;
entries[index++] = next;
entries[index ] = pos;
entries[index] = hash;
entries[index + 1] = next;
entries[index + 2] = pos;
// Validate comment if it exists.
// If the bytes representing the comment cannot be converted to
// a String via zcp.toString, an Exception will be thrown
if (clen > 0) {
int start = entryPos + nlen + elen;
zcp.toString(cen, start, clen);
zipCoder.toString(cen, start, clen);
}
} catch (Exception e) {
zerror("invalid CEN header (bad entry name or comment)");
@ -1389,34 +1422,47 @@ public class ZipFile implements ZipConstants, Closeable {
private int tablelen; // number of hash heads
/**
* A class representing a key to a ZIP file. A key is based
* on the file key if available, or the path value if the
* file key is not available. The key is also based on the
* file's last modified time to allow for cases where a ZIP
* file is re-opened after it has been modified.
* A class representing a key to the Source of a ZipFile.
* The Key is composed of:
* - The BasicFileAttributes.fileKey() if available, or the Path of the ZIP file
* if the fileKey() is not available.
* - The ZIP file's last modified time (to allow for cases
* where a ZIP file is re-opened after it has been modified).
* - The Charset that was provided when constructing the ZipFile instance.
* The unique combination of these components identifies a Source of a ZipFile.
*/
private static class Key {
final BasicFileAttributes attrs;
File file;
final boolean utf8;
private final BasicFileAttributes attrs;
private final File file;
// the Charset that was provided when constructing the ZipFile instance
private final Charset charset;
public Key(File file, BasicFileAttributes attrs, ZipCoder zc) {
/**
* Constructs a {@code Key} to a {@code Source} of a {@code ZipFile}
*
* @param file the ZIP file
* @param attrs the attributes of the ZIP file
* @param charset the Charset that was provided when constructing the ZipFile instance
*/
public Key(File file, BasicFileAttributes attrs, Charset charset) {
this.attrs = attrs;
this.file = file;
this.utf8 = zc.isUTF8();
this.charset = charset;
}
@Override
public int hashCode() {
long t = utf8 ? 0 : Long.MAX_VALUE;
long t = charset.hashCode();
t += attrs.lastModifiedTime().toMillis();
Object fk = attrs.fileKey();
return Long.hashCode(t) +
(fk != null ? fk.hashCode() : file.hashCode());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Key key) {
if (key.utf8 != utf8) {
if (!charset.equals(key.charset)) {
return false;
}
if (!attrs.lastModifiedTime().equals(key.attrs.lastModifiedTime())) {
@ -1440,12 +1486,12 @@ public class ZipFile implements ZipConstants, Closeable {
private static final java.nio.file.FileSystem builtInFS =
DefaultFileSystemProvider.theFileSystem();
static Source get(File file, boolean toDelete, ZipCoder zc) throws IOException {
static Source get(File file, boolean toDelete, ZipCoder zipCoder) throws IOException {
final Key key;
try {
key = new Key(file,
Files.readAttributes(builtInFS.getPath(file.getPath()),
BasicFileAttributes.class), zc);
BasicFileAttributes.class), zipCoder.charset());
} catch (InvalidPathException ipe) {
throw new IOException(ipe);
}
@ -1457,7 +1503,7 @@ public class ZipFile implements ZipConstants, Closeable {
return src;
}
}
src = new Source(key, toDelete, zc);
src = new Source(key, toDelete, zipCoder);
synchronized (files) {
Source prev = files.putIfAbsent(key, src);
@ -1479,8 +1525,7 @@ public class ZipFile implements ZipConstants, Closeable {
}
}
private Source(Key key, boolean toDelete, ZipCoder zc) throws IOException {
this.zc = zc;
private Source(Key key, boolean toDelete, ZipCoder zipCoder) throws IOException {
this.key = key;
if (toDelete) {
if (OperatingSystem.isWindows()) {
@ -1494,7 +1539,7 @@ public class ZipFile implements ZipConstants, Closeable {
this.zfile = new RandomAccessFile(key.file, "r");
}
try {
initCEN(-1);
initCEN(-1, zipCoder);
byte[] buf = new byte[4];
readFullyAt(buf, 0, 4, 0);
this.startsWithLoc = (LOCSIG(buf) == LOCSIG);
@ -1649,7 +1694,7 @@ public class ZipFile implements ZipConstants, Closeable {
}
// Reads ZIP file central directory.
private void initCEN(int knownTotal) throws IOException {
private void initCEN(final int knownTotal, final ZipCoder zipCoder) throws IOException {
// Prefer locals for better performance during startup
byte[] cen;
if (knownTotal == -1) {
@ -1711,13 +1756,15 @@ public class ZipFile implements ZipConstants, Closeable {
// This will only happen if the ZIP file has an incorrect
// ENDTOT field, which usually means it contains more than
// 65535 entries.
initCEN(countCENHeaders(cen));
initCEN(countCENHeaders(cen), zipCoder);
return;
}
int entryPos = pos + CENHDR;
// the ZipCoder for any non-UTF8 entries
final ZipCoder entryZipCoder = zipCoderFor(cen, pos, zipCoder);
// Checks the entry and adds values to entries[idx ... idx+2]
int nlen = checkAndAddEntry(pos, idx);
int nlen = checkAndAddEntry(pos, idx, entryZipCoder);
idx += 3;
// Adds name to metanames.
@ -1741,7 +1788,7 @@ public class ZipFile implements ZipConstants, Closeable {
try {
// Compute hash code of name from "META-INF/versions/{version)/{name}
int prefixLen = META_INF_VERSIONS_LEN + DecimalDigits.stringSize(version);
int hashCode = zipCoderForPos(pos).checkedHash(cen,
int hashCode = entryZipCoder.checkedHash(cen,
entryPos + prefixLen,
nlen - prefixLen);
// Register version for this hash code
@ -1786,9 +1833,10 @@ public class ZipFile implements ZipConstants, Closeable {
/*
* Returns the resolved name and position of the ZIP cen entry corresponding
* to the specified entry name, or {@code null} if not found.
* to the specified entry name, or {@code null} if not found.
*/
private EntryPos getEntryPos(String name, boolean addSlash) {
private EntryPos getEntryPos(final String name, final boolean addSlash,
final ZipCoder zipCoder) {
if (total == 0) {
return null;
}
@ -1807,8 +1855,7 @@ public class ZipFile implements ZipConstants, Closeable {
int noff = pos + CENHDR;
int nlen = CENNAM(cen, pos);
ZipCoder zc = zipCoderForPos(pos);
final ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
// Compare the lookup name with the name encoded in the CEN
switch (zc.compare(name, cen, noff, nlen, addSlash)) {
case ZipCoder.EXACT_MATCH:
@ -1834,16 +1881,6 @@ public class ZipFile implements ZipConstants, Closeable {
return null;
}
private ZipCoder zipCoderForPos(int pos) {
if (zc.isUTF8()) {
return zc;
}
if ((CENFLG(cen, pos) & USE_UTF8) != 0) {
return ZipCoder.UTF8;
}
return zc;
}
/**
* Returns true if the bytes represent a non-directory name
* beginning with "META-INF/", disregarding ASCII case.

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/*
* @test
* @bug 8355975
* @summary verify that the internal ZIP structure caching in java.util.zip.ZipFile
* uses the correct Charset when parsing the ZIP structure of a ZIP file
* @run junit ZipFileCharsetTest
*/
public class ZipFileCharsetTest {
private static final String ISO_8859_15_NAME = "ISO-8859-15";
/**
* The internal implementation of java.util.zip.ZipFile maintains a cache
* of the ZIP structure of each ZIP file that's currently open. This cache
* helps prevent repeat parsing of the ZIP structure of the same underlying
* ZIP file, every time a ZipFile instance is created for the same ZIP file.
* The cache uses an internal key to map a ZIP file to the corresponding
* ZIP structure that's cached.
* A ZipFile can be constructed by passing a Charset which will be used to
* decode the entry names (and comment) in a ZIP file.
* The test verifies that when multiple ZipFile instances are
* constructed using different Charsets but the same underlying ZIP file,
* then the internal caching implementation of ZipFile doesn't end up using
* a wrong Charset for parsing the ZIP structure of the ZIP file.
*/
@Test
void testCachedZipFileSource() throws Exception {
// ISO-8859-15 is not a standard charset in Java. We skip this test
// when it is unavailable
assumeTrue(Charset.availableCharsets().containsKey(ISO_8859_15_NAME),
"skipping test since " + ISO_8859_15_NAME + " charset isn't available");
// We choose the byte 0xA4 for entry name in the ZIP file.
// 0xA4 is "Euro sign" in ISO-8859-15 charset and
// "Currency sign (generic)" in ISO-8859-1 charset.
final byte[] entryNameBytes = new byte[]{(byte) 0xA4}; // intentional cast
final Charset euroSignCharset = Charset.forName(ISO_8859_15_NAME);
final Charset currencySignCharset = ISO_8859_1;
final String euroSign = new String(entryNameBytes, euroSignCharset);
final String currencySign = new String(entryNameBytes, currencySignCharset);
// create a ZIP file whose entry name is encoded using ISO-8859-15 charset
final Path zip = createZIP("euro", euroSignCharset, entryNameBytes);
// Construct a ZipFile instance using the (incorrect) charset ISO-8859-1.
// While that ZipFile instance is still open (and the ZIP file structure
// still cached), construct another instance for the same ZIP file, using
// the (correct) charset ISO-8859-15.
try (ZipFile incorrect = new ZipFile(zip.toFile(), currencySignCharset);
ZipFile correct = new ZipFile(zip.toFile(), euroSignCharset)) {
// correct encoding should resolve the entry name to euro sign
// and the entry should be thus be located
assertNotNull(correct.getEntry(euroSign), "euro sign entry missing in " + correct);
// correct encoding should not be able to find an entry name
// with the currency sign
assertNull(correct.getEntry(currencySign), "currency sign entry unexpectedly found in "
+ correct);
// incorrect encoding should resolve the entry name to currency sign
// and the entry should be thus be located by the currency sign name
assertNotNull(incorrect.getEntry(currencySign), "currency sign entry missing in "
+ incorrect);
// incorrect encoding should not be able to find an entry name
// with the euro sign
assertNull(incorrect.getEntry(euroSign), "euro sign entry unexpectedly found in "
+ incorrect);
}
}
/**
* Creates and return ZIP file whose entry names are encoded using the given {@code charset}
*/
private static Path createZIP(final String fileNamePrefix, final Charset charset,
final byte[] entryNameBytes) throws IOException {
final Path zip = Files.createTempFile(Path.of("."), fileNamePrefix, ".zip");
// create a ZIP file whose entry name(s) use the given charset
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zip), charset)) {
zos.putNextEntry(new ZipEntry(new String(entryNameBytes, charset)));
final byte[] entryContent = "doesnotmatter".getBytes(US_ASCII);
zos.write(entryContent);
zos.closeEntry();
}
return zip;
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.US_ASCII;
/*
* @test
* @bug 8347712
* @summary verify that different instances of java.util.zip.ZipFile do not share
* the same instance of (non-thread-safe) java.nio.charset.CharsetEncoder/CharsetDecoder
* @run junit ZipFileSharedSourceTest
*/
public class ZipFileSharedSourceTest {
static Path createZipFile(final Charset charset) throws Exception {
final Path zipFilePath = Files.createTempFile(Path.of("."), "8347712", ".zip");
try (OutputStream os = Files.newOutputStream(zipFilePath);
ZipOutputStream zos = new ZipOutputStream(os, charset)) {
final int numEntries = 10240;
for (int i = 1; i <= numEntries; i++) {
final ZipEntry entry = new ZipEntry("entry-" + i);
zos.putNextEntry(entry);
zos.write("foo bar".getBytes(US_ASCII));
zos.closeEntry();
}
}
return zipFilePath;
}
static List<Arguments> charsets() {
return List.of(
Arguments.of(StandardCharsets.UTF_8),
Arguments.of(StandardCharsets.ISO_8859_1),
Arguments.of(US_ASCII)
);
}
/**
* In this test, multiple concurrent threads each create an instance of java.util.zip.ZipFile
* with the given {@code charset} for the same underlying ZIP file. Each of the threads
* then iterate over the entries of their ZipFile instance. The test verifies that such access,
* where each thread is accessing an independent ZipFile instance corresponding to the same
* underlying ZIP file, doesn't lead to unexpected failures contributed by concurrent
* threads.
*/
@ParameterizedTest
@MethodSource("charsets")
void testMultipleZipFileInstances(final Charset charset) throws Exception {
final Path zipFilePath = createZipFile(charset);
final int numTasks = 200;
final CountDownLatch startLatch = new CountDownLatch(numTasks);
final List<Future<Void>> results = new ArrayList<>();
try (final ExecutorService executor =
Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())) {
for (int i = 0; i < numTasks; i++) {
final var task = new ZipEntryIteratingTask(zipFilePath, charset,
startLatch);
results.add(executor.submit(task));
}
System.out.println(numTasks + " tasks submitted, waiting for them to complete");
for (final Future<Void> f : results) {
f.get();
}
}
System.out.println("All " + numTasks + " tasks completed successfully");
}
private static final class ZipEntryIteratingTask implements Callable<Void> {
private final Path file;
private final Charset charset;
private final CountDownLatch startLatch;
private ZipEntryIteratingTask(final Path file, final Charset charset,
final CountDownLatch startLatch) {
this.file = file;
this.charset = charset;
this.startLatch = startLatch;
}
@Override
public Void call() throws Exception {
// let other tasks know we are ready to run
this.startLatch.countDown();
// wait for other tasks to be ready to run
this.startLatch.await();
// create a new instance of ZipFile and iterate over the entries
try (final ZipFile zf = new ZipFile(this.file.toFile(), this.charset)) {
final var entries = zf.entries();
while (entries.hasMoreElements()) {
final ZipEntry ze = entries.nextElement();
// additionally exercise the ZipFile.getEntry() method
zf.getEntry(ze.getName());
}
}
return null;
}
}
}