Skip to content

Commit 60159cf

Browse files
committed
8253952: Refine ZipOutputStream.putNextEntry() to recalculate ZipEntry's compressed size
Reviewed-by: lancea, alanb
1 parent 9359ff0 commit 60159cf

File tree

4 files changed

+231
-8
lines changed

4 files changed

+231
-8
lines changed

src/java.base/share/classes/java/util/jar/JarOutputStream.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,15 @@ public JarOutputStream(OutputStream out) throws IOException {
7676
/**
7777
* Begins writing a new JAR file entry and positions the stream
7878
* to the start of the entry data. This method will also close
79-
* any previous entry. The default compression method will be
80-
* used if no compression method was specified for the entry.
79+
* any previous entry.
80+
* <p>
81+
* The default compression method will be used if no compression
82+
* method was specified for the entry. When writing a compressed
83+
* (DEFLATED) entry, and the compressed size has not been explicitly
84+
* set with the {@link ZipEntry#setCompressedSize(long)} method,
85+
* then the compressed size will be set to the actual compressed
86+
* size after deflation.
87+
* <p>
8188
* The current time will be used if the entry has no set modification
8289
* time.
8390
*

src/java.base/share/classes/java/util/zip/ZipEntry.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public class ZipEntry implements ZipConstants, Cloneable {
5353
long crc = -1; // crc-32 of entry data
5454
long size = -1; // uncompressed size of entry data
5555
long csize = -1; // compressed size of entry data
56+
boolean csizeSet = false; // Only true if csize was explicitely set by
57+
// a call to setCompressedSize()
5658
int method = -1; // compression method
5759
int flag = 0; // general purpose flag
5860
byte[] extra; // optional extra field data for entry
@@ -127,6 +129,7 @@ public ZipEntry(ZipEntry e) {
127129
crc = e.crc;
128130
size = e.size;
129131
csize = e.csize;
132+
csizeSet = e.csizeSet;
130133
method = e.method;
131134
flag = e.flag;
132135
extra = e.extra;
@@ -447,6 +450,7 @@ public long getCompressedSize() {
447450
*/
448451
public void setCompressedSize(long csize) {
449452
this.csize = csize;
453+
this.csizeSet = true;
450454
}
451455

452456
/**

src/java.base/share/classes/java/util/zip/ZipOutputStream.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,15 @@ public void setLevel(int level) {
179179
/**
180180
* Begins writing a new ZIP file entry and positions the stream to the
181181
* start of the entry data. Closes the current entry if still active.
182+
* <p>
182183
* The default compression method will be used if no compression method
183-
* was specified for the entry, and the current time will be used if
184-
* the entry has no set modification time.
184+
* was specified for the entry. When writing a compressed (DEFLATED)
185+
* entry, and the compressed size has not been explicitly set with the
186+
* {@link ZipEntry#setCompressedSize(long)} method, then the compressed
187+
* size will be set to the actual compressed size after deflation.
188+
* <p>
189+
* The current time will be used if the entry has no set modification time.
190+
*
185191
* @param e the ZIP entry to be written
186192
* @throws ZipException if a ZIP format error has occurred
187193
* @throws IOException if an I/O error has occurred
@@ -203,11 +209,14 @@ public void putNextEntry(ZipEntry e) throws IOException {
203209
e.flag = 0;
204210
switch (e.method) {
205211
case DEFLATED:
206-
// store size, compressed size, and crc-32 in data descriptor
207-
// immediately following the compressed entry data
208-
if (e.size == -1 || e.csize == -1 || e.crc == -1)
212+
// If not set, store size, compressed size, and crc-32 in data
213+
// descriptor immediately following the compressed entry data.
214+
// Ignore the compressed size of a ZipEntry if it was implcitely set
215+
// while reading that ZipEntry from a ZipFile or ZipInputStream because
216+
// we can't know the compression level of the source zip file/stream.
217+
if (e.size == -1 || e.csize == -1 || e.crc == -1 || !e.csizeSet) {
209218
e.flag = 8;
210-
219+
}
211220
break;
212221
case STORED:
213222
// compressed size, uncompressed size, and crc-32 must all be
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/**
25+
* @test
26+
* @summary Test behaviour when copying ZipEntries between zip files.
27+
* @run main/othervm CopyZipFile
28+
*/
29+
30+
import java.io.File;
31+
import java.io.ByteArrayOutputStream;
32+
import java.io.FileInputStream;
33+
import java.io.FileNotFoundException;
34+
import java.io.FileOutputStream;
35+
import java.io.InputStream;
36+
import java.io.OutputStream;
37+
import java.util.Enumeration;
38+
import java.util.regex.Matcher;
39+
import java.util.regex.Pattern;
40+
import java.util.zip.CRC32;
41+
import java.util.zip.Deflater;
42+
import java.util.zip.ZipEntry;
43+
import java.util.zip.ZipException;
44+
import java.util.zip.ZipFile;
45+
import java.util.zip.ZipInputStream;
46+
import java.util.zip.ZipOutputStream;
47+
48+
public class CopyZipFile {
49+
private static final String ZIP_FILE = "first.zip";
50+
private static final String TEST_STRING = "TestTestTest";
51+
52+
private static void createZip(String zipFile) throws Exception {
53+
File f = new File(zipFile);
54+
f.deleteOnExit();
55+
try (OutputStream os = new FileOutputStream(f);
56+
ZipOutputStream zos = new ZipOutputStream(os)) {
57+
// First file will be compressed with DEFAULT_COMPRESSION (i.e. -1 or 6)
58+
zos.putNextEntry(new ZipEntry("test1.txt"));
59+
zos.write(TEST_STRING.getBytes());
60+
zos.closeEntry();
61+
// Second file won't be compressed at all (i.e. STORED)
62+
zos.setMethod(ZipOutputStream.STORED);
63+
ZipEntry ze = new ZipEntry("test2.txt");
64+
int length = TEST_STRING.length();
65+
ze.setSize(length);
66+
ze.setCompressedSize(length);
67+
CRC32 crc = new CRC32();
68+
crc.update(TEST_STRING.getBytes("utf8"), 0, length);
69+
ze.setCrc(crc.getValue());
70+
zos.putNextEntry(ze);
71+
zos.write(TEST_STRING.getBytes());
72+
// Third file will be compressed with NO_COMPRESSION (i.e. 0)
73+
zos.setMethod(ZipOutputStream.DEFLATED);
74+
zos.setLevel(Deflater.NO_COMPRESSION);
75+
zos.putNextEntry(new ZipEntry("test3.txt"));
76+
zos.write(TEST_STRING.getBytes());
77+
// Fourth file will be compressed with BEST_SPEED (i.e. 1)
78+
zos.setLevel(Deflater.BEST_SPEED);
79+
zos.putNextEntry(new ZipEntry("test4.txt"));
80+
zos.write(TEST_STRING.getBytes());
81+
// Fifth file will be compressed with BEST_COMPRESSION (i.e. 9)
82+
zos.setLevel(Deflater.BEST_COMPRESSION);
83+
zos.putNextEntry(new ZipEntry("test5.txt"));
84+
zos.write(TEST_STRING.getBytes());
85+
}
86+
}
87+
88+
public static void main(String args[]) throws Exception {
89+
// By default, ZipOutputStream creates zip files with Local File Headers
90+
// without size, compressedSize and crc values and an extra Data
91+
// Descriptor (see https://en.wikipedia.org/wiki/Zip_(file_format)
92+
// after the data belonging to that entry with these values if in the
93+
// corresponding ZipEntry one of the size, compressedSize or crc fields is
94+
// equal to '-1' (which is the default for newly created ZipEntries).
95+
createZip(ZIP_FILE);
96+
97+
// Now read all the entries of the newly generated zip file with a ZipInputStream
98+
// and copy them to a new zip file with the help of a ZipOutputStream.
99+
// This only works reliably because the generated zip file has no values for the
100+
// size, compressedSize and crc values of a zip entry in the local file header and
101+
// therefore the ZipEntry objects created by ZipOutputStream.getNextEntry() will have
102+
// all these fields set to '-1'.
103+
ZipEntry entry;
104+
byte[] buf = new byte[512];
105+
try (InputStream is = new FileInputStream(ZIP_FILE);
106+
ZipInputStream zis = new ZipInputStream(is);
107+
OutputStream os = new ByteArrayOutputStream();
108+
ZipOutputStream zos = new ZipOutputStream(os)) {
109+
while((entry = zis.getNextEntry())!=null) {
110+
// ZipInputStream.getNextEntry() only reads the Local File Header of a zip entry,
111+
// so for the zip file we've just generated the ZipEntry fields 'size', 'compressedSize`
112+
// and 'crc' for deflated entries should be uninitialized (i.e. '-1').
113+
System.out.println(
114+
String.format("name=%s, clen=%d, len=%d, crc=%d",
115+
entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc()));
116+
if (entry.getMethod() == ZipEntry.DEFLATED &&
117+
(entry.getCompressedSize() != -1 || entry.getSize() != -1 || entry.getCrc() != -1)) {
118+
throw new Exception("'size', 'compressedSize' and 'crc' shouldn't be initialized at this point.");
119+
}
120+
zos.putNextEntry(entry);
121+
zis.transferTo(zos);
122+
// After all the data belonging to a zip entry has been inflated (i.e. after ZipInputStream.read()
123+
// returned '-1'), it is guaranteed that the ZipInputStream will also have consumed the Data
124+
// Descriptor (if any) after the data and will have updated the 'size', 'compressedSize' and 'crc'
125+
// fields of the ZipEntry object.
126+
System.out.println(
127+
String.format("name=%s, clen=%d, len=%d, crc=%d\n",
128+
entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc()));
129+
if (entry.getCompressedSize() == -1 || entry.getSize() == -1) {
130+
throw new Exception("'size' and 'compressedSize' must be initialized at this point.");
131+
}
132+
}
133+
}
134+
135+
// Now we read all the entries of the initially generated zip file with the help
136+
// of the ZipFile class. The ZipFile class reads all the zip entries from the Central
137+
// Directory which must have accurate information for size, compressedSize and crc.
138+
// This means that all ZipEntry objects returned from ZipFile will have correct
139+
// settings for these fields.
140+
// If the compression level was different in the initial zip file (which we can't find
141+
// out any more now because the zip file format doesn't record this information) the
142+
// size of the re-compressed entry we are writing to the ZipOutputStream might differ
143+
// from the original compressed size recorded in the ZipEntry. This would result in an
144+
// "invalid entry compressed size" ZipException if ZipOutputStream wouldn't ignore
145+
// the implicitely set compressed size attribute of ZipEntries read from a ZipFile
146+
// or ZipInputStream.
147+
try (OutputStream os = new ByteArrayOutputStream();
148+
ZipOutputStream zos = new ZipOutputStream(os);
149+
ZipFile zf = new ZipFile(ZIP_FILE)) {
150+
Enumeration<? extends ZipEntry> entries = zf.entries();
151+
while (entries.hasMoreElements()) {
152+
entry = entries.nextElement();
153+
System.out.println(
154+
String.format("name=%s, clen=%d, len=%d, crc=%d\n",
155+
entry.getName(), entry.getCompressedSize(),
156+
entry.getSize(), entry.getCrc()));
157+
if (entry.getCompressedSize() == -1 || entry.getSize() == -1) {
158+
throw new Exception("'size' and 'compressedSize' must be initialized at this point.");
159+
}
160+
InputStream is = zf.getInputStream(entry);
161+
zos.putNextEntry(entry);
162+
is.transferTo(zos);
163+
zos.closeEntry();
164+
}
165+
}
166+
167+
// The compressed size attribute of a ZipEntry shouldn't be ignored if it was set
168+
// explicitely by calling ZipEntry.setCpompressedSize()
169+
try (OutputStream os = new ByteArrayOutputStream();
170+
ZipOutputStream zos = new ZipOutputStream(os);
171+
ZipFile zf = new ZipFile(ZIP_FILE)) {
172+
Enumeration<? extends ZipEntry> entries = zf.entries();
173+
while (entries.hasMoreElements()) {
174+
try {
175+
entry = entries.nextElement();
176+
entry.setCompressedSize(entry.getCompressedSize());
177+
InputStream is = zf.getInputStream(entry);
178+
zos.putNextEntry(entry);
179+
is.transferTo(zos);
180+
zos.closeEntry();
181+
if ("test3.txt".equals(entry.getName())) {
182+
throw new Exception(
183+
"Should throw a ZipException if ZipEntry.setCpompressedSize() was called.");
184+
}
185+
} catch (ZipException ze) {
186+
if ("test1.txt".equals(entry.getName()) || "test2.txt".equals(entry.getName())) {
187+
throw new Exception(
188+
"Shouldn't throw a ZipExcpetion for STORED files or files compressed with DEFAULT_COMPRESSION");
189+
}
190+
// Hack to fix and close the offending zip entry with the correct compressed size.
191+
// The exception message is something like:
192+
// "invalid entry compressed size (expected 12 but got 7 bytes)"
193+
// and we need to extract the second integer.
194+
Pattern cSize = Pattern.compile("\\d+");
195+
Matcher m = cSize.matcher(ze.getMessage());
196+
m.find();
197+
m.find();
198+
entry.setCompressedSize(Integer.parseInt(m.group()));
199+
}
200+
}
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)