jdk-24/test/jdk/java/util/zip/ZipFile/InvalidBytesInEntryNameOrComment.java

329 lines
14 KiB
Java

/*
* 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();
}
}