| /* | |
| * Copyright (c) 2023, 2024, 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. | |
| * | |
| */ | |
| import org.junit.jupiter.api.BeforeEach; | |
| import org.junit.jupiter.api.Test; | |
| import org.junit.jupiter.params.ParameterizedTest; | |
| import org.junit.jupiter.params.provider.Arguments; | |
| import org.junit.jupiter.params.provider.MethodSource; | |
| import java.io.FileInputStream; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.nio.ByteBuffer; | |
| import java.nio.ByteOrder; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.util.Arrays; | |
| import java.util.Formatter; | |
| import java.util.stream.Stream; | |
| import java.util.zip.*; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| /** | |
| * @test | |
| * @bug 8301873 8321156 | |
| * @summary Validate that a ZipException is thrown when a ZIP file with | |
| * invalid UTF-8 byte sequences in the name or comment fields is opened via | |
| * ZipFile or traversed via ZipInputStream. | |
| * Also validate that ZipFile::getComment will return null with invalid UTF-8 | |
| * byte sequences in the ZIP file comment | |
| * @run junit InvalidBytesInEntryNameOrComment | |
| */ | |
| public class InvalidBytesInEntryNameOrComment { | |
| // Zip file that is created and used by the test | |
| public static final Path ZIP_FILE = Path.of("BadName.zip"); | |
| // Example invalid UTF-8 byte sequence | |
| private static final byte[] INVALID_UTF8_BYTE_SEQUENCE = {(byte) 0xF0, (byte) 0xA4, (byte) 0xAD}; | |
| // Expected error message when an invalid entry name or entry comment is | |
| // encountered when accessing a CEN Header | |
| private static final String CEN_BAD_ENTRY_NAME_OR_COMMENT = "invalid CEN header (bad entry name or comment)"; | |
| // Expected error message when an invalid entry name is encountered when | |
| // accessing a LOC Header | |
| private static final String LOC_HEADER_BAD_ENTRY_NAME = "invalid LOC header (bad entry name)"; | |
| // Zip file comment starting offset | |
| private static final int ZIP_FILE_COMMENT_OFFSET = 0x93; | |
| // CEN Header offset for the entry comment to be modified | |
| private static final int CEN_FILE_HEADER_FILE_COMMENT_STARTING_OFFSET = 0x6D; | |
| // CEN Header offset for the entry name to be modified | |
| private static final int CEN_FILE_HEADER_FILENAME_STARTING_OFFSET = 0x66; | |
| // LOC Header offset for the entry name to be modified | |
| private static final int LOC_FILE_HEADER_FILENAME_STARTING_OFFSET = 0x1e; | |
| // CEN Entry comment | |
| public static final String ENTRY_COMMENT = "entryComment"; | |
| // Entry name to be modified/validated | |
| public static final String ENTRY_NAME = "entryName"; | |
| // Zip file comment to be modified/validated | |
| public static final String ZIP_FILE_COMMENT = "ZipFileComment"; | |
| // Buffer used to massage the byte array containing the Zip File | |
| private ByteBuffer buffer; | |
| // Array used to copy VALID_ZIP into prior to each test run | |
| private byte[] zipArray; | |
| /** | |
| * Byte array representing a valid Zip file prior modifying the CEN/LOC | |
| * entry name, CEN entry comment or Zip file comment with an invalid | |
| * UTF-8 byte sequence. | |
| * See the createZipByteArray method which was used to create the original | |
| * Zip file | |
| * ----------------#1-------------------- | |
| * [Central Directory Header] | |
| * 0x3a: Signature : 0x02014b50 | |
| * 0x3e: Created Zip Spec : 0x14 [2.0] | |
| * 0x3f: Created OS : 0x0 [MS-DOS] | |
| * 0x40: VerMadeby : 0x14 [0, 2.0] | |
| * 0x41: VerExtract : 0x14 [2.0] | |
| * 0x42: Flag : 0x808 | |
| * 0x44: Method : 0x8 [DEFLATED] | |
| * 0x46: Last Mod Time : 0x58506664 [Fri Feb 16 12:51:08 EST 2024] | |
| * 0x4a: CRC : 0xd202ef8d | |
| * 0x4e: Compressed Size : 0x3 | |
| * 0x52: Uncompressed Size: 0x1 | |
| * 0x56: Name Length : 0x9 | |
| * 0x58: Extra Length : 0x0 | |
| * 0x5a: Comment Length : 0xc | |
| * 0x5c: Disk Start : 0x0 | |
| * 0x5e: Attrs : 0x0 | |
| * 0x60: AttrsEx : 0x0 | |
| * 0x64: Loc Header Offset: 0x0 | |
| * 0x68: File Name : entryName | |
| * 0x71: Comment : [entryComment] | |
| * [Local File Header] | |
| * 0x0: Signature : 0x04034b50 | |
| * 0x4: Version : 0x14 [2.0] | |
| * 0x6: Flag : 0x808 | |
| * 0x8: Method : 0x8 [DEFLATED] | |
| * 0xa: LastMTime : 0x58506664 [Fri Feb 16 12:51:08 EST 2024] | |
| * 0xe: CRC : 0x0 | |
| * 0x12: CSize : 0x0 | |
| * 0x16: Size : 0x0 | |
| * 0x1a: Name Length : 0x9 [entryName] | |
| * 0x1c: ExtraLength : 0x0 | |
| * 0x1e: File Name : [entryName] | |
| * [End Central Directory Header] | |
| * 0x7d: Signature : 0x06054b50 | |
| * 0x85: Disk Entries: 0x1 | |
| * 0x87: Total Entries: 0x1 | |
| * 0x89: CEN Size : 0x43 | |
| * 0x8d: Offset CEN : 0x3a | |
| * 0x91: Comment Len : 0xe | |
| * 0x93: Comment : [ZipFileComment] | |
| */ | |
| public static byte[] VALID_ZIP = { | |
| (byte) 0x50, (byte) 0x4b, (byte) 0x3, (byte) 0x4, (byte) 0x14, (byte) 0x0, (byte) 0x8, (byte) 0x8, | |
| (byte) 0x8, (byte) 0x0, (byte) 0x7d, (byte) 0x6f, (byte) 0x50, (byte) 0x58, (byte) 0x0, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x9, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x65, (byte) 0x6e, | |
| (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x4e, (byte) 0x61, (byte) 0x6d, (byte) 0x65, (byte) 0x63, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x50, (byte) 0x4b, (byte) 0x7, (byte) 0x8, (byte) 0x8d, (byte) 0xef, | |
| (byte) 0x2, (byte) 0xd2, (byte) 0x3, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x50, (byte) 0x4b, (byte) 0x1, (byte) 0x2, (byte) 0x14, (byte) 0x0, | |
| (byte) 0x14, (byte) 0x0, (byte) 0x8, (byte) 0x8, (byte) 0x8, (byte) 0x0, (byte) 0x7d, (byte) 0x6f, | |
| (byte) 0x50, (byte) 0x58, (byte) 0x8d, (byte) 0xef, (byte) 0x2, (byte) 0xd2, (byte) 0x3, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x9, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0xc, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, | |
| (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, | |
| (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x4e, (byte) 0x61, (byte) 0x6d, | |
| (byte) 0x65, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x43, (byte) 0x6f, | |
| (byte) 0x6d, (byte) 0x6d, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x50, (byte) 0x4b, (byte) 0x5, | |
| (byte) 0x6, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0x0, (byte) 0x1, | |
| (byte) 0x0, (byte) 0x43, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x3a, (byte) 0x0, (byte) 0x0, | |
| (byte) 0x0, (byte) 0xe, (byte) 0x0, (byte) 0x5a, (byte) 0x69, (byte) 0x70, (byte) 0x46, (byte) 0x69, | |
| (byte) 0x6c, (byte) 0x65, (byte) 0x43, (byte) 0x6f, (byte) 0x6d, (byte) 0x6d, (byte) 0x65, (byte) 0x6e, | |
| (byte) 0x74, | |
| }; | |
| /** | |
| * Delete the Zip file if it exists prior to each run and create a copy | |
| * of the byte array representing a valid ZIP file to be used by each test run | |
| * | |
| * @throws IOException if an error occurs | |
| */ | |
| @BeforeEach | |
| public void setupTest() throws IOException { | |
| Files.deleteIfExists(ZIP_FILE); | |
| zipArray = Arrays.copyOf(VALID_ZIP, VALID_ZIP.length); | |
| buffer = ByteBuffer.wrap(zipArray).order(ByteOrder.LITTLE_ENDIAN); | |
| } | |
| /** | |
| * The DataProvider of CEN offsets to modify with an invalid UTF-8 byte | |
| * sequence | |
| * | |
| * @return Arguments used in each test run | |
| */ | |
| private static Stream<Arguments> CENCommentOffsets() { | |
| return Stream.of( | |
| // Entry's name starting offset | |
| Arguments.of(CEN_FILE_HEADER_FILENAME_STARTING_OFFSET), | |
| // Entry's comment starting offset | |
| Arguments.of(CEN_FILE_HEADER_FILE_COMMENT_STARTING_OFFSET) | |
| ); | |
| } | |
| /** | |
| * Validate that the original Zip file can be opened via ZipFile. | |
| * @throws IOException if an error occurs | |
| */ | |
| @Test | |
| public void testValidEntryNameAndComment() throws IOException { | |
| Files.write(ZIP_FILE, zipArray); | |
| try (ZipFile zf = new ZipFile(ZIP_FILE.toFile())) { | |
| var comment = zf.getComment(); | |
| assertEquals(ZIP_FILE_COMMENT, comment); | |
| } | |
| } | |
| /** | |
| * Validate that the original Zip file can be opened and traversed via | |
| * ZipinputStream::getNextEntry. | |
| * @throws IOException if an error occurs | |
| */ | |
| @Test | |
| public void traverseZipWithZipInputStreamTest() throws IOException { | |
| Files.write(ZIP_FILE, zipArray); | |
| try (ZipInputStream zis = new ZipInputStream(new FileInputStream(ZIP_FILE.toFile()))) { | |
| ZipEntry ze; | |
| while ((ze = zis.getNextEntry()) != null) { | |
| assertEquals(ENTRY_NAME, ze.getName()); | |
| } | |
| } | |
| } | |
| /** | |
| * Validate that a ZipException is thrown when an entry name or entry comment | |
| * within a CEN file header contains an invalid UTF-8 byte sequence. | |
| * | |
| * @param offset the offset to the file name or file comment within the CEN | |
| * file header | |
| * @throws IOException if an error occurs | |
| */ | |
| @ParameterizedTest | |
| @MethodSource("CENCommentOffsets") | |
| public void testInValidEntryNameOrComment(int offset) | |
| throws IOException { | |
| createInvalidUTFEntryInZipFile(offset); | |
| Throwable ex = assertThrows(ZipException.class, () -> | |
| { | |
| try (ZipFile zf = new ZipFile(ZIP_FILE.toFile())) {}; | |
| } | |
| ); | |
| assertEquals(CEN_BAD_ENTRY_NAME_OR_COMMENT, ex.getMessage()); | |
| } | |
| /** | |
| * Validate that a null is returned from ZipFile::getComment when the | |
| * comment contains an invalid UTF-8 byte sequence. | |
| * @throws IOException if an error occurs | |
| */ | |
| @Test | |
| public void testInValidZipFileComment() throws IOException { | |
| createInvalidUTFEntryInZipFile(ZIP_FILE_COMMENT_OFFSET); | |
| try (ZipFile zf = new ZipFile(ZIP_FILE.toFile())) { | |
| assertNull(zf.getComment()); | |
| } | |
| } | |
| /** | |
| * Validate that a ZipException is thrown when an entry name | |
| * within a LOC file header contains an invalid UTF-8 byte sequence. | |
| * @throws IOException if an error occurs | |
| */ | |
| @Test | |
| public void invalidZipInputStreamTest() throws IOException { | |
| createInvalidUTFEntryInZipFile(LOC_FILE_HEADER_FILENAME_STARTING_OFFSET); | |
| Throwable ex = assertThrows(ZipException.class, () -> | |
| { | |
| try (ZipInputStream zis = | |
| new ZipInputStream(new FileInputStream(ZIP_FILE.toFile()))) { | |
| zis.getNextEntry(); | |
| }; | |
| }); | |
| assertEquals(LOC_HEADER_BAD_ENTRY_NAME, ex.getMessage()); | |
| } | |
| /** | |
| * Utility method which modifies a Zip file starting at the specified | |
| * offset to include an invalid UTF-8 byte sequence. | |
| * | |
| * @param offset starting offset within the Zip file to modify | |
| * @throws IOException if an error occurs | |
| */ | |
| private void createInvalidUTFEntryInZipFile(int offset) throws IOException { | |
| buffer.put(offset, INVALID_UTF8_BYTE_SEQUENCE, 0, | |
| INVALID_UTF8_BYTE_SEQUENCE.length); | |
| Files.write(ZIP_FILE, zipArray); | |
| } | |
| /** | |
| * Utility method which creates the Zip file used by the tests and | |
| * converts Zip file to byte array declaration. | |
| * | |
| * @throws IOException if an error occurs | |
| */ | |
| private void createZipByteArray() throws IOException { | |
| ZipOutputStream zos = new ZipOutputStream( | |
| new FileOutputStream(ZIP_FILE.toFile())); | |
| zos.setComment(ZIP_FILE_COMMENT); | |
| ZipEntry entry = new ZipEntry(ENTRY_NAME); | |
| entry.setComment(ENTRY_COMMENT); | |
| zos.putNextEntry(entry); | |
| zos.write(new byte[1]); | |
| zos.closeEntry(); | |
| zos.close(); | |
| // Now create the byte array entry declaration | |
| var fooJar = Files.readAllBytes(ZIP_FILE); | |
| var result = createByteArray(fooJar, "VALID_ZIP"); | |
| System.out.println(result); | |
| } | |
| /** | |
| * Utility method which takes a byte array and converts to byte array | |
| * declaration. For example: | |
| * {@snippet : | |
| * var fooJar = Files.readAllBytes(Path.of("foo.jar")); | |
| * var result = createByteArray(fooJar,"FOOBYTES"); | |
| * System.out.println(result); | |
| * } | |
| * | |
| * @param bytes A byte array used to create a byte array declaration | |
| * @param name Name to be used in the byte array declaration | |
| * @return The formatted byte array declaration | |
| */ | |
| public static String createByteArray(byte[] bytes, String name) { | |
| StringBuilder sb = new StringBuilder(bytes.length * 5); | |
| Formatter fmt = new Formatter(sb); | |
| fmt.format(" public static byte[] %s = {", name); | |
| final int linelen = 8; | |
| for (int i = 0; i < bytes.length; i++) { | |
| if (i % linelen == 0) { | |
| fmt.format("%n "); | |
| } | |
| fmt.format(" (byte) 0x%x,", bytes[i] & 0xff); | |
| } | |
| fmt.format("%n };%n"); | |
| return sb.toString(); | |
| } | |
| } |