1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
|
/*
* Copyright Amazon.com Inc. 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.
*/
/**
* @test
* @bug 8253952
* @summary Test behaviour when copying ZipEntries between zip files.
* @run junit CopyZipFile
*/
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.*;
import static org.junit.jupiter.api.Assertions.*;
public class CopyZipFile {
// ZIP file created in this test
private Path zip = Path.of("first.zip");
// The content to put in each entry
private static final byte[] TEST_STRING = "TestTestTest".getBytes(StandardCharsets.UTF_8);
/**
* Create the sample ZIP file used in this test, including a STORED entry
* and DEFLATE entries with various compression levels.
* @throws IOException if an unexpected IOException occurs
*/
@BeforeEach
public void createZip() throws IOException {
// By default, ZipOutputStream creates zip files with Local File Headers
// without size, compressed size and crc values and an extra Data
// Descriptor (see https://en.wikipedia.org/wiki/Zip_(file_format)
// after the data belonging to that entry with these values if in the
// corresponding ZipEntry one of the size, compressedSize or crc fields is
// equal to '-1' (which is the default for newly created ZipEntries).
try (OutputStream os = Files.newOutputStream(zip) ;
ZipOutputStream zos = new ZipOutputStream(os)) {
// First file will be compressed with DEFAULT_COMPRESSION (i.e. -1 or 6)
zos.setLevel(Deflater.DEFAULT_COMPRESSION);
zos.putNextEntry(new ZipEntry("DEFAULT_COMPRESSION.txt"));
zos.write(TEST_STRING);
// Second file won't be compressed at all (i.e. STORED)
zos.setMethod(ZipOutputStream.STORED);
ZipEntry ze = new ZipEntry("STORED.txt");
ze.setSize(TEST_STRING.length);
ze.setCompressedSize(TEST_STRING.length);
CRC32 crc = new CRC32();
crc.update(TEST_STRING);
ze.setCrc(crc.getValue());
zos.putNextEntry(ze);
zos.write(TEST_STRING);
// Third file will be compressed with NO_COMPRESSION (i.e. 0)
zos.setMethod(ZipOutputStream.DEFLATED);
zos.setLevel(Deflater.NO_COMPRESSION);
zos.putNextEntry(new ZipEntry("NO_COMPRESSION.txt"));
zos.write(TEST_STRING);
// Fourth file will be compressed with BEST_SPEED (i.e. 1)
zos.setLevel(Deflater.BEST_SPEED);
zos.putNextEntry(new ZipEntry("BEST_SPEED.txt"));
zos.write(TEST_STRING);
// Fifth file will be compressed with BEST_COMPRESSION (i.e. 9)
zos.setLevel(Deflater.BEST_COMPRESSION);
zos.putNextEntry(new ZipEntry("BEST_COMPRESSION.txt"));
zos.write(TEST_STRING);
}
}
/**
* Delete the ZIP file produced by this test
* @throws IOException if an unexpected IOException occurs
*/
@AfterEach
public void cleanup() throws IOException {
Files.deleteIfExists(zip);
}
/**
* Read all entries using ZipInputStream.getNextEntry and copy them
* to a new zip file using ZipOutputStream.putNextEntry. This only works
* reliably because the input zip file has no values for the size, compressedSize
* and crc values of streamed zip entries in the local file header and
* therefore the ZipEntry objects created by ZipOutputStream.getNextEntry
* will have all these fields set to '-1'.
*
* @throws IOException if an unexpected IOException occurs
*/
@Test
public void copyFromZipInputStreamToZipOutputStream() throws IOException {
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip));
ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream())) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// ZipInputStream.getNextEntry() only reads the Local File Header of a zip entry,
// so for the zip file we've just generated the ZipEntry fields 'size', 'compressedSize`
// and 'crc' for deflated entries should be uninitialized (i.e. '-1').
System.out.println(
String.format("name=%s, clen=%d, len=%d, crc=%d",
entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc()));
if (entry.getMethod() == ZipEntry.DEFLATED) {
// Expect size, compressed size and crc to not be initialized at this point
assertEquals(-1, entry.getCompressedSize());
assertEquals(-1, entry.getSize());
assertEquals(-1, entry.getCrc());
}
zos.putNextEntry(entry);
zis.transferTo(zos);
// After all the data belonging to a zip entry has been inflated (i.e. after ZipInputStream.read()
// returned '-1'), it is guaranteed that the ZipInputStream will also have consumed the Data
// Descriptor (if any) after the data and will have updated the 'size', 'compressedSize' and 'crc'
// fields of the ZipEntry object.
System.out.println(
String.format("name=%s, clen=%d, len=%d, crc=%d\n",
entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc()));
// Expect size, compressed size and crc to be initialized at this point
assertNotEquals(-1, entry.getCompressedSize());
assertNotEquals(-1, entry.getSize());
assertNotEquals(-1, entry.getCrc());
}
}
}
/**
* Read all entries using the ZipFile class and copy them to a new zip file
* using ZipOutputStream.putNextEntry.
* The ZipFile class reads all the zip entries from the Central
* Directory, which has accurate information for size, compressedSize and crc.
* This means that all ZipEntry objects returned from ZipFile will have correct
* settings for these fields.
* If the compression level was different in the input zip file (which we can't know
* because the zip file format doesn't record this information), the
* size of the re-compressed entry we are writing to the ZipOutputStream might differ
* from the original compressed size recorded in the ZipEntry. This would result in an
* "invalid entry compressed size" ZipException if ZipOutputStream wouldn't ignore
* the implicitely set compressed size attribute of ZipEntries read from a ZipFile
* or ZipInputStream.
* @throws IOException if an unexpected IOException occurs
*/
@Test
public void copyFromZipFileToZipOutputStream() throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream());
ZipFile zf = new ZipFile(zip.toFile())) {
ZipEntry entry;
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
entry = entries.nextElement();
System.out.println(
String.format("name=%s, clen=%d, len=%d, crc=%d\n",
entry.getName(), entry.getCompressedSize(),
entry.getSize(), entry.getCrc()));
// Expect size, compressed size and crc to be initialized at this point
assertNotEquals(-1, entry.getCompressedSize());
assertNotEquals(-1, entry.getSize());
assertNotEquals(-1, entry.getCrc());
zos.putNextEntry(entry);
try (InputStream is = zf.getInputStream(entry)) {
is.transferTo(zos);
}
zos.closeEntry();
}
}
}
/**
* If the compressed size is set explicitly using ZipEntry.setCompressedSize(),
* then the entry will be restreamed with a data descriptor and the compressed size
* recomputed. If the source compression level was different from the target compression
* level, the compressed sizes may differ and a ZipException will be thrown
* when the entry is closed in ZipOutputStream.closeEntry
*
* @throws IOException if an unexpected IOException is thrown
*/
@Test
public void explicitCompressedSizeWithDifferentCompressionLevels() throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream());
ZipFile zf = new ZipFile(zip.toFile())) {
// Be explicit about the default compression level
zos.setLevel(Deflater.DEFAULT_COMPRESSION);
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
// Explicitly setting the compressed size will disable data descriptors
// and enable validation that the compressed size in the ZipEntry matches the
// actual compressed size written by ZipOutputStream
entry.setCompressedSize(entry.getCompressedSize());
try (InputStream is = zf.getInputStream(entry)) {
zos.putNextEntry(entry);
is.transferTo(zos);
// Some compression levels lead to unexpected recompressed sizes when closing the entry
switch (entry.getName()) {
case "DEFAULT_COMPRESSION.txt" -> {
// DEFAULT_COMPRESSION matches expected size
zos.closeEntry();
}
case "STORED.txt" -> {
// STORED should not throw
zos.closeEntry();
}
case "NO_COMPRESSION.txt", "BEST_SPEED.txt" -> {
// NO_COMPRESSION and BEST_SPEED should lead to an unexpected recompressed size
ZipException ze = assertThrows(ZipException.class, () -> {
zos.closeEntry();
});
// Hack to fix and close the offending zip entry with the correct recompressed size.
// The exception message is something like:
// "invalid entry compressed size (expected 12 but got 7 bytes)"
// and we need to extract the second integer.
Pattern cSize = Pattern.compile("\\d+");
Matcher m = cSize.matcher(ze.getMessage());
m.find();
m.find();
entry.setCompressedSize(Integer.parseInt(m.group()));
zos.closeEntry();
}
case "BEST_COMPRESSION.txt" -> {
// BEST_COMPRESSION produces the same compressed
// size as DEFAULT_COMPRESSION for sample content
zos.closeEntry();
}
default -> {
throw new IllegalArgumentException("Unexpected entry " + entry.getName());
}
}
}
}
}
}
}
|