File: CopyZipFile.java

package info (click to toggle)
openjdk-21 21.0.8%2B9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 823,976 kB
  • sloc: java: 5,613,338; xml: 1,643,607; cpp: 1,296,296; ansic: 420,291; asm: 404,850; objc: 20,994; sh: 15,271; javascript: 11,245; python: 6,895; makefile: 2,362; perl: 357; awk: 351; sed: 172; jsp: 24; csh: 3
file content (268 lines) | stat: -rw-r--r-- 12,973 bytes parent folder | download | duplicates (9)
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());
                        }
                    }
                }
            }
        }
    }
}