184 lines
6.4 KiB
Java
184 lines
6.4 KiB
Java
|
/*
|
||
|
* Copyright (c) 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.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
import org.testng.annotations.BeforeTest;
|
||
|
import org.testng.annotations.Test;
|
||
|
|
||
|
import java.io.ByteArrayOutputStream;
|
||
|
import java.io.FileOutputStream;
|
||
|
import java.io.IOException;
|
||
|
import java.nio.ByteBuffer;
|
||
|
import java.nio.ByteOrder;
|
||
|
import java.nio.channels.FileChannel;
|
||
|
import java.nio.file.Path;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.zip.ZipEntry;
|
||
|
import java.util.zip.ZipException;
|
||
|
import java.util.zip.ZipFile;
|
||
|
import java.util.zip.ZipOutputStream;
|
||
|
|
||
|
import static org.testng.Assert.assertEquals;
|
||
|
import static org.testng.Assert.expectThrows;
|
||
|
|
||
|
/**
|
||
|
* @test
|
||
|
* @summary Validate that opening ZIP files files with invalid UTF-8
|
||
|
* byte sequences in the name or comment fields fails with ZipException
|
||
|
* @run testng/othervm InvalidBytesInEntryNameOrComment
|
||
|
*/
|
||
|
public class InvalidBytesInEntryNameOrComment {
|
||
|
|
||
|
// Offsets for navigating the CEN fields
|
||
|
private static final int EOC_OFF = 6; // Offset from EOF to find CEN offset
|
||
|
private static final int CEN_HDR = 45; // Size of a CEN header
|
||
|
private static final int NLEN = 28; // Name length
|
||
|
private static final int ELEN = 30; // Extra length
|
||
|
private static final int CLEN = 32; // Comment length
|
||
|
|
||
|
// Example invalid UTF-8 byte sequence
|
||
|
private static final byte[] INVALID_UTF8_BYTE_SEQUENCE = {(byte) 0xF0, (byte) 0xA4, (byte) 0xAD};
|
||
|
|
||
|
// Expected ZipException regex
|
||
|
private static final String BAD_ENTRY_NAME_OR_COMMENT = "invalid CEN header (bad entry name or comment)";
|
||
|
|
||
|
// ZIP file with invalid name field
|
||
|
private Path invalidName;
|
||
|
|
||
|
// ZIP file with invalid comment field
|
||
|
private Path invalidComment;
|
||
|
|
||
|
@BeforeTest
|
||
|
public void setup() throws IOException {
|
||
|
// Create a ZIP file with valid name and comment fields
|
||
|
byte[] templateZip = templateZIP();
|
||
|
|
||
|
// Create a ZIP with a CEN name field containing an invalid byte sequence
|
||
|
invalidName = invalidName("invalid-name.zip", templateZip);
|
||
|
|
||
|
// Create a ZIP with a CEN comment field containing an invalid byte sequence
|
||
|
invalidComment = invalidComment("invalid-comment.zip", templateZip);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Opening a ZipFile with an invalid UTF-8 byte sequence in
|
||
|
* the name field of a CEN file header should throw a
|
||
|
* ZipException with "bad entry name or comment"
|
||
|
*/
|
||
|
@Test
|
||
|
public void shouldRejectInvalidName() throws IOException {
|
||
|
ZipException ex = expectThrows(ZipException.class, () -> {
|
||
|
new ZipFile(invalidName.toFile());
|
||
|
});
|
||
|
assertEquals(ex.getMessage(), BAD_ENTRY_NAME_OR_COMMENT);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Opening a ZipFile with an invalid UTF-8 byte sequence in
|
||
|
* the comment field of a CEN file header should throw a
|
||
|
* ZipException with "bad entry name or comment"
|
||
|
*/
|
||
|
@Test
|
||
|
public void shouldRejectInvalidComment() throws IOException {
|
||
|
ZipException ex = expectThrows(ZipException.class, () -> {
|
||
|
new ZipFile(invalidComment.toFile());
|
||
|
});
|
||
|
assertEquals(ex.getMessage(), BAD_ENTRY_NAME_OR_COMMENT);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make a valid ZIP file used as a template for invalid files
|
||
|
*/
|
||
|
private byte[] templateZIP() throws IOException {
|
||
|
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||
|
try (ZipOutputStream zo = new ZipOutputStream(bout)) {
|
||
|
ZipEntry commentEntry = new ZipEntry("file");
|
||
|
commentEntry.setComment("Comment");
|
||
|
zo.putNextEntry(commentEntry);
|
||
|
}
|
||
|
return bout.toByteArray();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make a ZIP with invalid bytes in the CEN name field
|
||
|
*/
|
||
|
private Path invalidName(String name, byte[] template) throws IOException {
|
||
|
ByteBuffer buffer = copyTemplate(template);
|
||
|
int off = cenStart(buffer);
|
||
|
// Name field starts here
|
||
|
int noff = off + CEN_HDR;
|
||
|
|
||
|
// Write invalid bytes
|
||
|
buffer.put(noff, INVALID_UTF8_BYTE_SEQUENCE, 0, INVALID_UTF8_BYTE_SEQUENCE.length);
|
||
|
return writeFile(name, buffer);
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make a copy of the ZIP template and wrap it in a little-endian
|
||
|
* ByteBuffer
|
||
|
*/
|
||
|
private ByteBuffer copyTemplate(byte[] template) {
|
||
|
return ByteBuffer.wrap(Arrays.copyOf(template, template.length))
|
||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make a ZIP with invalid bytes in the CEN comment field
|
||
|
*/
|
||
|
private Path invalidComment(String name, byte[] template) throws IOException {
|
||
|
ByteBuffer buffer = copyTemplate(template);
|
||
|
int off = cenStart(buffer);
|
||
|
// Need to skip past the length of the name and extra fields
|
||
|
int nlen = buffer.getShort(off + NLEN);
|
||
|
int elen = buffer.getShort(off + ELEN);
|
||
|
|
||
|
// Comment field starts here
|
||
|
int coff = off + CEN_HDR + nlen + elen;
|
||
|
|
||
|
// Write invalid bytes
|
||
|
buffer.put(coff, INVALID_UTF8_BYTE_SEQUENCE, 0, INVALID_UTF8_BYTE_SEQUENCE.length);
|
||
|
return writeFile(name, buffer);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Finds the offset of the start of the CEN directory
|
||
|
*/
|
||
|
private int cenStart(ByteBuffer buffer) {
|
||
|
return buffer.getInt(buffer.capacity() - EOC_OFF);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Utility to write a ByteBuffer to disk
|
||
|
*/
|
||
|
private Path writeFile(String name, ByteBuffer buffer) throws IOException {
|
||
|
Path zip = Path.of(name);
|
||
|
try (FileChannel ch = new FileOutputStream(zip.toFile()).getChannel()) {
|
||
|
buffer.rewind();
|
||
|
ch.write(buffer);
|
||
|
}
|
||
|
return zip;
|
||
|
}
|
||
|
}
|