File: CorruptedZipFiles.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 (332 lines) | stat: -rw-r--r-- 11,969 bytes parent folder | download | duplicates (2)
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/*
 * Copyright (c) 2005, 2023, 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.
 */

/* @test
 * @bug 4770745 6218846 6218848 6237956 8313765
 * @summary test for correct detection and reporting of corrupted zip files
 * @author Martin Buchholz
 * @run junit CorruptedZipFiles
 */


import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import static java.util.zip.ZipFile.*;
import static org.junit.jupiter.api.Assertions.*;

public class CorruptedZipFiles {

    /*
     * Byte array holding a valid template ZIP.
     *
     * The 'good' ZIP file has the following structure:
     *
     * 0000 LOCAL HEADER #1       04034B50
     * 0004 Extract Zip Spec      14 '2.0'
     * 0005 Extract OS            00 'MS-DOS'
     * 0006 General Purpose Flag  0808
     *      [Bits 1-2]            0 'Normal Compression'
     *      [Bit  3]              1 'Streamed'
     *      [Bit 11]              1 'Language Encoding'
     * 0008 Compression Method    0008 'Deflated'
     * 000A Last Mod Time         567F7D07 'Fri Mar 31 15:40:14 2023'
     * 000E CRC                   00000000
     * 0012 Compressed Length     00000000
     * 0016 Uncompressed Length   00000000
     * 001A Filename Length       0001
     * 001C Extra Length          0000
     * 001E Filename              'x'
     * 001F PAYLOAD               ...
     *
     * 0022 STREAMING DATA HEADER 08074B50
     * 0026 CRC                   8CDC1683
     * 002A Compressed Length     00000003
     * 002E Uncompressed Length   00000001
     *
     * 0032 CENTRAL HEADER #1     02014B50
     * 0036 Created Zip Spec      14 '2.0'
     * 0037 Created OS            00 'MS-DOS'
     * 0038 Extract Zip Spec      14 '2.0'
     * 0039 Extract OS            00 'MS-DOS'
     * 003A General Purpose Flag  0808
     *      [Bits 1-2]            0 'Normal Compression'
     *      [Bit  3]              1 'Streamed'
     *      [Bit 11]              1 'Language Encoding'
     * 003C Compression Method    0008 'Deflated'
     * 003E Last Mod Time         567F7D07 'Fri Mar 31 15:40:14 2023'
     * 0042 CRC                   8CDC1683
     * 0046 Compressed Length     00000003
     * 004A Uncompressed Length   00000001
     * 004E Filename Length       0001
     * 0050 Extra Length          0000
     * 0052 Comment Length        0000
     * 0054 Disk Start            0000
     * 0056 Int File Attributes   0000
     *      [Bit 0]               0 'Binary Data'
     * 0058 Ext File Attributes   00000000
     * 005C Local Header Offset   00000000
     * 0060 Filename              'x'
     *
     * 0061 END CENTRAL HEADER    06054B50
     * 0065 Number of this disk   0000
     * 0067 Central Dir Disk no   0000
     * 0069 Entries in this disk  0001
     * 006B Total Entries         0001
     * 006D Size of Central Dir   0000002F
     * 0071 Offset to Central Dir 00000032
     * 0075 Comment Length        0000
     *
     */
    private static byte[] template;

    // Copy of the template ZIP for modification by each test
    private byte[] copy;

    // Litte-endian ByteBuffer for manipulating the ZIP copy
    private ByteBuffer buffer;

    // Some well-known locations in the ZIP
    private static int endpos, cenpos, locpos;

    // The path used when reading/writing the corrupted ZIP to disk
    private Path zip = Path.of("corrupted.zip");

    /*
     * Make a sample ZIP and calculate some known offsets into this ZIP
     */
    @BeforeAll
    public static void setup() throws IOException {
        // Make a ZIP with a single entry
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (ZipOutputStream zos = new ZipOutputStream(out)) {
            ZipEntry e = new ZipEntry("x");
            zos.putNextEntry(e);
            zos.write((int)'x');
        }
        template = out.toByteArray();
        // ByteBuffer for reading fields from the ZIP
        ByteBuffer buffer = ByteBuffer.wrap(template).order(ByteOrder.LITTLE_ENDIAN);

        // Calculate the offset of the End of central directory record
        endpos = template.length - ENDHDR;
        // Look up the offet of the Central directory header
        cenpos = buffer.getShort(endpos + ENDOFF);
        // Look up the offset of the corresponding Local file header
        locpos = buffer.getShort(cenpos + CENOFF);

        // Run some sanity checks on the valid ZIP:
        assertEquals(ENDSIG, buffer.getInt(endpos),"Where's ENDSIG?");
        assertEquals(CENSIG, buffer.getInt(cenpos),"Where's CENSIG?");
        assertEquals(LOCSIG, buffer.getInt(locpos),"Where's LOCSIG?");
        assertEquals(buffer.getShort(cenpos+CENNAM),
                buffer.getShort(locpos+LOCNAM),
                "Name field length mismatch");
        assertEquals(buffer.getShort(cenpos+CENEXT),
                buffer.getShort( locpos+LOCEXT),
                "Extra field length mismatch");
    }

    /*
     * Make a copy safe to modify by each test
     */
    @BeforeEach
    public void makeCopy() {
        copy = template.clone();
        buffer = ByteBuffer.wrap(copy).order(ByteOrder.LITTLE_ENDIAN);
    }

    /*
     * Delete the ZIP file produced after each test method
     */
    @AfterEach
    public void cleanup() throws IOException {
        Files.deleteIfExists(zip);
    }

    /*
     * A ZipException is thrown when the 'End of Central Directory'
     * (END) header has a CEN size exceeding past the offset of the END record
     */
    @Test
    public void excessiveCENSize() throws IOException {
        buffer.putInt(endpos+ENDSIZ, 0xff000000);
        assertZipException(".*bad central directory size.*");
    }

    /*
     * A ZipException is thrown when the 'End of Central Directory'
     * (END) header has a CEN offset with an invalid value.
     */
    @Test
    public void excessiveCENOffset() throws IOException {
        buffer.putInt(endpos+ENDOFF, 0xff000000);
        assertZipException(".*bad central directory offset.*");
    }

    /*
     * A ZipException is thrown when a CEN header has an unexpected signature
     */
    @Test
    public void invalidCENSignature() throws IOException {
        int existingSignature = buffer.getInt(cenpos);
        buffer.putInt(cenpos, existingSignature +1);
        assertZipException(".*bad signature.*");
    }

    /*
     * A ZipException is thrown when a CEN header has the
     * 'general purpose bit flag 0' ('encrypted') set.
     */
    @Test
    public void encryptedEntry() throws IOException {
        copy[cenpos+CENFLG] |= 1;
        assertZipException(".*encrypted entry.*");
    }

    /*
     * A ZipException is thrown when a CEN header has a file name
     *  length which makes the CEN header overflow into the
     * 'End of central directory' record.
     */
    @Test
    public void excessiveFileNameLength() throws IOException {
        short existingNameLength = buffer.getShort(cenpos + CENNAM);
        buffer.putShort(cenpos+CENNAM, (short) (existingNameLength + 1));
        assertZipException(".*bad header size.*");
    }

    /*
     * A ZipException is thrown when a CEN header has a
     * file name length which makes the CEN header overflow into the
     * 'End of central directory' record.
     */
    @Test
    public void excessiveFileNameLength2() throws IOException {
        buffer.putShort(cenpos + CENNAM, (short) 0xfdfd);
        assertZipException(".*bad header size.*");
    }

    /*
     * A ZipException is thrown if the last CEN header is not immediately
     * followed by the start of the 'End of central directory' record
     */
    @Test
    public void insufficientFilenameLength() throws IOException {
        short existingNameLength = buffer.getShort(cenpos + CENNAM);
        buffer.putShort(cenpos+CENNAM, (short) (existingNameLength - 1));
        assertZipException(".*bad header size.*");
    }

    /*
     * A ZipException is thrown if a CEN header has an
     * extra field length which makes the CEN header overflow into the
     * End of central directory record.
     */
    @Test
    public void excessiveExtraFieldLength() throws IOException {
        buffer.put(cenpos+CENEXT, (byte) 0xff);
        buffer.put(cenpos+CENEXT+1, (byte) 0xff);
        assertZipException(".*extra data field size too long.*");
    }

    /*
     * A ZipException is thrown if a CEN header has an
     * extra field length which makes the CEN header overflow into the
     * End of central directory record.
     */
    @Test
    public void excessiveExtraFieldLength2() throws IOException {
        buffer.putShort(cenpos+CENEXT, (short) 0xfdfd);
        assertZipException(".*extra data field size too long.*");
    }

    /*
     * A ZipException is thrown when a CEN header has a comment length
     * which overflows into the 'End of central directory' record
     */
    @Test
    public void excessiveCommentLength() throws IOException {
        short existingCommentLength = buffer.getShort(cenpos + CENCOM);
        buffer.putShort(cenpos+CENCOM, (short) (existingCommentLength + 1));
        assertZipException(".*bad header size.*");
    }

    /*
     * A ZipException is thrown when a CEN header has a
     * compression method field which is unsupported by the implementation
     */
    @Test
    public void unsupportedCompressionMethod() throws IOException {
        copy[cenpos+CENHOW] = 2;
        assertZipException(".*bad compression method.*");
    }

    /*
     * A ZipException is thrown when a LOC header has an unexpected signature
     */
    @Test
    public void invalidLOCSignature() throws IOException {
        int existingSignatur = buffer.getInt(locpos);
        buffer.putInt(locpos, existingSignatur +1);
        assertZipException(".*bad signature.*");
    }

    /*
     * Assert that opening a ZIP file and consuming the entry's
     * InputStream using the ZipFile API fails with a ZipException
     * with a message matching the given pattern.
     *
     * The ZIP file opened is the contents of the 'copy' byte array.
     */
    void assertZipException(String msgPattern) throws IOException {

        Files.write(zip, copy);

        ZipException ex = assertThrows(ZipException.class, () -> {
            try (ZipFile zf = new ZipFile(zip.toFile())) {
                try (InputStream is = zf.getInputStream(new ZipEntry("x"))) {
                    is.transferTo(OutputStream.nullOutputStream());
                }
            }
        });
        assertTrue(ex.getMessage().matches(msgPattern),
                "Unexpected ZipException message: " + ex.getMessage());

    }
}