Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d0518e7
8347712: IllegalStateException on multithreaded ZipFile access with n…
jaikiran Mar 11, 2025
b9734fa
tiny typo fix in newly introduced documentation
jaikiran Mar 11, 2025
b6486e7
rename field
jaikiran Mar 12, 2025
935b04e
add code comment
jaikiran Mar 12, 2025
0ac8c0e
merge latest from master branch
jaikiran Mar 19, 2025
c2f29ae
Eirik's inputs - replace useUTF8Coder() with zipCoderFor()
jaikiran Mar 19, 2025
bca4d6b
Eirik's suggestion - update test to even call ZipFile.getEntry()
jaikiran Mar 19, 2025
3257392
rename entryNameCharset to charset in the test
jaikiran Mar 19, 2025
83ac59f
Eirik's suggestion - update test method comment
jaikiran Mar 19, 2025
88d4c77
Alan's suggestion - trim the javadoc of (internal) ZipCoder class
jaikiran Mar 23, 2025
306d40e
Alan's suggestion - change code comment about Source class being thre…
jaikiran Mar 23, 2025
4e3f8e7
improve code comment for ZipFile.zipCoder
jaikiran Mar 23, 2025
23b41e9
merge latest from master branch
jaikiran Apr 1, 2025
9d2066c
merge latest from master branch
jaikiran Apr 8, 2025
040b283
merge latest from master branch
jaikiran Apr 19, 2025
18d8cb5
merge latest from master branch
jaikiran Apr 22, 2025
b995118
merge latest from master branch
jaikiran Apr 28, 2025
9d9de1a
8355975: introduce a test for 8355975
jaikiran Apr 30, 2025
c1fe098
merge latest from master branch
jaikiran Apr 30, 2025
e7d969f
fix comment typo
jaikiran Apr 30, 2025
9a29b96
Eirik's review about code comments
jaikiran Apr 30, 2025
71cb978
Lance's review - update code comment in the test
jaikiran Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/java.base/share/classes/java/util/zip/ZipCoder.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -38,7 +38,10 @@
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "thread safe" feature is already implied by the comment on UTF8: "Encoding/decoding is stateless". So I recommend just mentioning that a ZipCoder may carry states, and a ZipCoder obtained from ZipCoder.get should only be used locally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @liach here. The fact that one ZipCoder is stateless and thread-safe is a class-internal implementation detail and not information which any call site of this API makes use of.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I recommend just mentioning that a ZipCoder may carry states, and a ZipCoder obtained from ZipCoder.get should only be used locally.

The UTF8ZipCoder may not just be accessed through the ZipCoder.UTF8 field. It could even be obtained through ZipCoder.get(UTF_8). So the ZipCode returned by ZipCoder.get() can still be used concurrently (if it is for UTF-8).

The fact that one ZipCoder is stateless and thread-safe is a class-internal implementation detail and not information which any call site of this API makes use of.

The ZipCoder itself is an internal implementation detail (it's a package protected class), so the javadoc that we are discussing here won't be published by the java.base module. I believe that the thread safety aspect of the ZipCoder instances, even if it for some specific charsets, is an important detail even for internal call sites to be aware of. In fact, the current issue that we are fixing shows that this detail might have helped prevent this issue.

I think the brief comment that we have added here, in its current form, is good enough. So unless either of you or others feel strongly about it, I would like to keep it in its current form.

*/
class ZipCoder {

Expand Down Expand Up @@ -174,6 +177,13 @@ protected CharsetDecoder decoder() {
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()
Expand Down
147 changes: 92 additions & 55 deletions src/java.base/share/classes/java/util/zip/ZipFile.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -198,7 +200,8 @@ public ZipFile(File file, int mode, Charset charset) throws IOException
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();
Expand Down Expand Up @@ -265,7 +268,7 @@ public String getComment() {
// 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;
}
Expand All @@ -288,7 +291,7 @@ public ZipEntry getEntry(String name) {
// 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);
}
Expand Down Expand Up @@ -328,7 +331,7 @@ public InputStream getInputStream(ZipEntry entry) throws IOException {
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 {
Expand Down Expand Up @@ -363,6 +366,35 @@ public InputStream getInputStream(ZipEntry entry) throws IOException {
}
}

/**
* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried putting an instance method on ZipFile instead as it has access to the zip coder. That would give you better abstraction and avoid needing to introduce "fallback" as that is confusing to see here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Alan that an instance method would provide better abstraction and avoid the somewhat clumsy "fallback" parameter introduced in my previous patch.

Here's a patch exploring the instance method:

Index: src/java.base/share/classes/java/util/zip/ZipFile.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/java.base/share/classes/java/util/zip/ZipFile.java b/src/java.base/share/classes/java/util/zip/ZipFile.java
--- a/src/java.base/share/classes/java/util/zip/ZipFile.java	(revision 83ac59fcd292c21bbff4e0ac243a6bf07b4b21dc)
+++ b/src/java.base/share/classes/java/util/zip/ZipFile.java	(date 1742719028475)
@@ -202,7 +202,7 @@
         long t0 = System.nanoTime();
 
         this.zipCoder = ZipCoder.get(charset);
-        this.res = new CleanableResource(this, zipCoder, file, mode);
+        this.res = new CleanableResource(this, file, mode);
 
         PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0);
         PerfCounter.getZipFileCount().increment();
@@ -292,7 +292,7 @@
             // 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, zipCoder);
+            EntryPos pos = res.zsrc.getEntryPos(name, true, this);
             if (pos != null) {
                 entry = getZipEntry(pos.name, pos.pos);
             }
@@ -332,7 +332,7 @@
             if (Objects.equals(lastEntryName, entry.name)) {
                 pos = lastEntryPos;
             } else {
-                EntryPos entryPos = zsrc.getEntryPos(entry.name, false, zipCoder);
+                EntryPos entryPos = zsrc.getEntryPos(entry.name, false, this);
                 if (entryPos != null) {
                     pos = entryPos.pos;
                 } else {
@@ -374,26 +374,25 @@
      * <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}.
+     * entry doesn't require UTF-8, then this method returns the
+     * {@code ZipCoder} of the ZipFile.
      *
      * @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,
+    private ZipCoder zipCoderFor(final byte[] cen, final int pos) {
+        if (zipCoder.isUTF8()) {
+            // the 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;
+            return zipCoder;
         }
         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;
+        return zipCoder;
     }
 
     private static class InflaterCleanupAction implements Runnable {
@@ -594,7 +593,7 @@
     private String getEntryName(int pos) {
         byte[] cen = res.zsrc.cen;
         int nlen = CENNAM(cen, pos);
-        ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
+        ZipCoder zc = zipCoderFor(cen, pos);
         return zc.toString(cen, pos + CENHDR, nlen);
     }
 
@@ -665,7 +664,7 @@
         }
         if (clen != 0) {
             int start = pos + CENHDR + nlen + elen;
-            ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
+            ZipCoder zc = zipCoderFor(cen, pos);
             e.comment = zc.toString(cen, start, clen);
         }
         lastEntryName = e.name;
@@ -697,12 +696,11 @@
 
         Source zsrc;
 
-        CleanableResource(ZipFile zf, ZipCoder zipCoder, File file, int mode) throws IOException {
-            assert zipCoder != null : "null ZipCoder";
+        CleanableResource(ZipFile zf, File file, int mode) throws IOException {
             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, zipCoder);
+            this.zsrc = Source.get(file, (mode & OPEN_DELETE) != 0, zf);
         }
 
         void clean() {
@@ -1492,12 +1490,12 @@
         private static final java.nio.file.FileSystem builtInFS =
                 DefaultFileSystemProvider.theFileSystem();
 
-        static Source get(File file, boolean toDelete, ZipCoder zipCoder) throws IOException {
+        static Source get(File file, boolean toDelete, ZipFile zipFile) throws IOException {
             final Key key;
             try {
                 key = new Key(file,
                         Files.readAttributes(builtInFS.getPath(file.getPath()),
-                                BasicFileAttributes.class), zipCoder.charset());
+                                BasicFileAttributes.class), zipFile.zipCoder.charset());
             } catch (InvalidPathException ipe) {
                 throw new IOException(ipe);
             }
@@ -1509,7 +1507,7 @@
                     return src;
                 }
             }
-            src = new Source(key, toDelete, zipCoder);
+            src = new Source(key, toDelete, zipFile);
 
             synchronized (files) {
                 Source prev = files.putIfAbsent(key, src);
@@ -1531,7 +1529,7 @@
             }
         }
 
-        private Source(Key key, boolean toDelete, ZipCoder zipCoder) throws IOException {
+        private Source(Key key, boolean toDelete, ZipFile zipFile) throws IOException {
             this.key = key;
             if (toDelete) {
                 if (OperatingSystem.isWindows()) {
@@ -1545,7 +1543,7 @@
                 this.zfile = new RandomAccessFile(key.file, "r");
             }
             try {
-                initCEN(-1, zipCoder);
+                initCEN(-1, zipFile);
                 byte[] buf = new byte[4];
                 readFullyAt(buf, 0, 4, 0);
                 this.startsWithLoc = (LOCSIG(buf) == LOCSIG);
@@ -1700,7 +1698,7 @@
         }
 
         // Reads ZIP file central directory.
-        private void initCEN(final int knownTotal, final ZipCoder zipCoder) throws IOException {
+        private void initCEN(final int knownTotal, final ZipFile zipFile) throws IOException {
             // Prefer locals for better performance during startup
             byte[] cen;
             if (knownTotal == -1) {
@@ -1762,13 +1760,13 @@
                     // 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), zipCoder);
+                    initCEN(countCENHeaders(cen), zipFile);
                     return;
                 }
 
                 int entryPos = pos + CENHDR;
                 // the ZipCoder for any non-UTF8 entries
-                final ZipCoder entryZipCoder = zipCoderFor(cen, pos, zipCoder);
+                final ZipCoder entryZipCoder = zipFile.zipCoderFor(cen, pos);
                 // Checks the entry and adds values to entries[idx ... idx+2]
                 int nlen = checkAndAddEntry(pos, idx, entryZipCoder);
                 idx += 3;
@@ -1842,7 +1840,7 @@
          * to the specified entry name, or {@code null} if not found.
          */
         private EntryPos getEntryPos(final String name, final boolean addSlash,
-                                     final ZipCoder zipCoder) {
+                                     final ZipFile zipFile) {
             if (total == 0) {
                 return null;
             }
@@ -1861,7 +1859,7 @@
                     int noff = pos + CENHDR;
                     int nlen = CENNAM(cen, pos);
 
-                    final ZipCoder zc = zipCoderFor(cen, pos, zipCoder);
+                    final ZipCoder zc = zipFile.zipCoderFor(cen, pos);
                     // Compare the lookup name with the name encoded in the CEN
                     switch (zc.compare(name, cen, noff, nlen, addSlash)) {
                         case ZipCoder.EXACT_MATCH:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Alan,

Have you tried putting an instance method on ZipFile instead as it has access to the zip coder. That would give you better abstraction and avoid needing to introduce "fallback" as that is confusing to see here.

When I experimented with a fix for this issue, several weeks back, my initial version included a variant of what Eirik shows as a diff in his comment. I decided to shelve that in favour of what I currently have in my PR. The root of the issue is that we use the ZipCoder at two categorically different moments in the lifecycle of a ZipFile instance. Once when the ZipFile is still being constructed and once after the ZipFile instance is fully constructed.

In the constructor of the ZipFile, the code leads to the static Source class. A ZipCoder that's an instance member/method of ZipFile can be used but that would then mean that various code paths that spawn out of the Source class, while parsing the CEN, may end up operating on a ZipFile instance (while obtaining the ZipCoder) that isn't fully constructed yet. It would not be immediately obvious while reading or updating such code that the ZipFile instance may not yet have been fully constructed. So to reduce such usage I decided to pursue this alternate way where using these static methods would prevent such access to the ZipFile instance. It has it's own drawbacks like you note about the confusing method parameters. But I felt that this is better of the two approaches.

If you think we should consider the instance method approach, then let me know and I'll experiment more and see if any issues related to that can be minimized.

Maybe in some larger future work this entire code path can be redone in a manner that a ZipFile instance that isn't yet constructed, doesn't have such tight integration with the Source/cache class.

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;
Expand Down Expand Up @@ -561,7 +593,7 @@ public Stream<? extends ZipEntry> stream() {
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);
}

Expand Down Expand Up @@ -632,7 +664,7 @@ private ZipEntry getZipEntry(String name, int pos) {
}
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;
Expand Down Expand Up @@ -664,11 +696,12 @@ private static class CleanableResource implements Runnable {

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() {
Expand Down Expand Up @@ -1109,6 +1142,7 @@ public void setExternalFileAttributes(ZipEntry ze, int externalFileAttributes) {
// 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
Expand All @@ -1121,7 +1155,6 @@ private static class Source {
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;

Expand Down Expand Up @@ -1157,8 +1190,9 @@ private static class Source {
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;
Expand Down Expand Up @@ -1196,21 +1230,20 @@ private int checkAndAddEntry(int pos, int index)
}

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unrelated to the issue at hand. Perhaps better left for a separate PR, backed by a benchmark.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a performance related change. I merely marked the method parameters as final and the index method parameter was being updated here.

entries[index + 1] = next;
entries[index + 2] = pos;
Comment on lines +1238 to +1240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
entries[index] = hash;
entries[index + 1] = next;
entries[index + 2] = pos;
entries[index ] = hash;
entries[index + 1] = next;
entries[index + 2] = pos;

Aligned code is more readable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Shaojin, leaving one or more spaces before a closing ] bracket of array access contradicts the style that I've seen used often in the code. At least it's not a common style used in this area of the code in the JDK.

I think it's fine in the current form and changing it to align with subsequent lines isn't necessary.

// 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)");
Expand Down Expand Up @@ -1389,34 +1422,47 @@ private static boolean isZip64ExtBlockSizeValid(int blockSize, long csize,
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;

public Key(File file, BasicFileAttributes attrs, ZipCoder zc) {
private final BasicFileAttributes attrs;
private final File file;
// the Charset that was provided when constructing the ZipFile instance
private final Charset charset;

/**
* 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())) {
Expand All @@ -1440,12 +1486,12 @@ public boolean equals(Object obj) {
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);
}
Expand All @@ -1457,7 +1503,7 @@ static Source get(File file, boolean toDelete, ZipCoder zc) throws IOException {
return src;
}
}
src = new Source(key, toDelete, zc);
src = new Source(key, toDelete, zipCoder);

synchronized (files) {
Source prev = files.putIfAbsent(key, src);
Expand All @@ -1479,8 +1525,7 @@ static void release(Source src) throws IOException {
}
}

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()) {
Expand All @@ -1494,7 +1539,7 @@ private Source(Key key, boolean toDelete, ZipCoder zc) throws IOException {
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);
Expand Down Expand Up @@ -1649,7 +1694,7 @@ private End findEND() throws IOException {
}

// 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) {
Expand Down Expand Up @@ -1711,13 +1756,15 @@ private void initCEN(int knownTotal) throws IOException {
// 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.
Expand All @@ -1741,7 +1788,7 @@ private void initCEN(int knownTotal) throws IOException {
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
Expand Down Expand Up @@ -1786,9 +1833,10 @@ private static void zerror(String msg) throws ZipException {

/*
* 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;
}
Expand All @@ -1807,8 +1855,7 @@ private EntryPos getEntryPos(String name, boolean addSlash) {
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:
Expand All @@ -1834,16 +1881,6 @@ private EntryPos getEntryPos(String name, boolean addSlash) {
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.
Expand Down
Loading