8255380: (zipfs) ZipFileSystem::readExtra can fail if zipinfo-time is not set to false

Reviewed-by: redestad
This commit is contained in:
Lance Andersen 2020-11-03 19:20:42 +00:00
parent 88ee973334
commit 6606e0909c
2 changed files with 282 additions and 39 deletions

View File

@ -2907,11 +2907,16 @@ class ZipFileSystem extends FileSystem {
// read NTFS, UNIX and ZIP64 data from cen.extra
private void readExtra(ZipFileSystem zipfs) throws IOException {
// Note that Section 4.5, Extensible data fields, of the PKWARE ZIP File
// Format Specification does not mandate a specific order for the
// data in the extra field, therefore Zip FS cannot assume the data
// is written in the same order by Zip libraries as Zip FS.
if (extra == null)
return;
int elen = extra.length;
int off = 0;
int newOff = 0;
boolean hasZip64LocOffset = false;
while (off + 4 < elen) {
// extra spec: HeaderID+DataSize+Data
int pos = off;
@ -2955,7 +2960,7 @@ class ZipFileSystem extends FileSystem {
ctime = winToJavaTime(LL(extra, pos + 20));
break;
case EXTID_EXTT:
// spec says the Extened timestamp in cen only has mtime
// spec says the Extended timestamp in cen only has mtime
// need to read the loc to get the extra a/ctime, if flag
// "zipinfo-time" is not specified to false;
// there is performance cost (move up to loc and read) to
@ -2965,44 +2970,15 @@ class ZipFileSystem extends FileSystem {
mtime = unixToJavaTime(LG(extra, pos + 1));
break;
}
byte[] buf = new byte[LOCHDR];
if (zipfs.readFullyAt(buf, 0, buf.length , locoff)
!= buf.length)
throw new ZipException("loc: reading failed");
if (!locSigAt(buf, 0))
throw new ZipException("loc: wrong sig ->"
+ Long.toString(getSig(buf, 0), 16));
int locElen = LOCEXT(buf);
if (locElen < 9) // EXTT is at least 9 bytes
break;
int locNlen = LOCNAM(buf);
buf = new byte[locElen];
if (zipfs.readFullyAt(buf, 0, buf.length , locoff + LOCHDR + locNlen)
!= buf.length)
throw new ZipException("loc extra: reading failed");
int locPos = 0;
while (locPos + 4 < buf.length) {
int locTag = SH(buf, locPos);
int locSZ = SH(buf, locPos + 2);
locPos += 4;
if (locTag != EXTID_EXTT) {
locPos += locSZ;
continue;
}
int end = locPos + locSZ - 4;
int flag = CH(buf, locPos++);
if ((flag & 0x1) != 0 && locPos <= end) {
mtime = unixToJavaTime(LG(buf, locPos));
locPos += 4;
}
if ((flag & 0x2) != 0 && locPos <= end) {
atime = unixToJavaTime(LG(buf, locPos));
locPos += 4;
}
if ((flag & 0x4) != 0 && locPos <= end) {
ctime = unixToJavaTime(LG(buf, locPos));
}
break;
// If the LOC offset is 0xFFFFFFFF, then we need to read the
// LOC offset from the EXTID_ZIP64 extra data. Therefore
// wait until all of the CEN extra data fields have been processed
// prior to reading the LOC extra data field in order to obtain
// the Info-ZIP Extended Timestamp.
if (locoff != ZIP64_MINVAL) {
readLocEXTT(zipfs);
} else {
hasZip64LocOffset = true;
}
break;
default: // unknown tag
@ -3011,12 +2987,66 @@ class ZipFileSystem extends FileSystem {
}
off += (sz + 4);
}
// We need to read the LOC extra data and the LOC offset was obtained
// from the EXTID_ZIP64 field.
if (hasZip64LocOffset) {
readLocEXTT(zipfs);
}
if (newOff != 0 && newOff != extra.length)
extra = Arrays.copyOf(extra, newOff);
else
extra = null;
}
/**
* Read the LOC extra field to obtain the Info-ZIP Extended Timestamp fields
* @param zipfs The Zip FS to use
* @throws IOException If an error occurs
*/
private void readLocEXTT(ZipFileSystem zipfs) throws IOException {
byte[] buf = new byte[LOCHDR];
if (zipfs.readFullyAt(buf, 0, buf.length , locoff)
!= buf.length)
throw new ZipException("loc: reading failed");
if (!locSigAt(buf, 0))
throw new ZipException("R"
+ Long.toString(getSig(buf, 0), 16));
int locElen = LOCEXT(buf);
if (locElen < 9) // EXTT is at least 9 bytes
return;
int locNlen = LOCNAM(buf);
buf = new byte[locElen];
if (zipfs.readFullyAt(buf, 0, buf.length , locoff + LOCHDR + locNlen)
!= buf.length)
throw new ZipException("loc extra: reading failed");
int locPos = 0;
while (locPos + 4 < buf.length) {
int locTag = SH(buf, locPos);
int locSZ = SH(buf, locPos + 2);
locPos += 4;
if (locTag != EXTID_EXTT) {
locPos += locSZ;
continue;
}
int end = locPos + locSZ - 4;
int flag = CH(buf, locPos++);
if ((flag & 0x1) != 0 && locPos <= end) {
mtime = unixToJavaTime(LG(buf, locPos));
locPos += 4;
}
if ((flag & 0x2) != 0 && locPos <= end) {
atime = unixToJavaTime(LG(buf, locPos));
locPos += 4;
}
if ((flag & 0x4) != 0 && locPos <= end) {
ctime = unixToJavaTime(LG(buf, locPos));
}
break;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(1024);

View File

@ -0,0 +1,213 @@
/*
* Copyright (c) 2020, 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.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipFile;
import static java.lang.String.format;
/**
* @test
* @bug 8255380
* @summary Test that Zip FS can access the LOC offset from the Zip64 extra field
* @modules jdk.zipfs
* @requires (os.family == "linux") | (os.family == "mac")
* @run testng/manual TestLocOffsetFromZip64EF
*/
public class TestLocOffsetFromZip64EF {
private static final String ZIP_FILE_NAME = "LargeZipTest.zip";
// File that will be created with a size greater than 0xFFFFFFFF
private static final String LARGE_FILE_NAME = "LargeZipEntry.txt";
// File that will be created with a size less than 0xFFFFFFFF
private static final String SMALL_FILE_NAME = "SmallZipEntry.txt";
// The size (4GB) of the large file to be created
private static final long LARGE_FILE_SIZE = 4L * 1024L * 1024L * 1024L;
/**
* Create the files used by this test
* @throws IOException if an error occurs
*/
@BeforeClass
public void setUp() throws IOException {
System.out.println("In setup");
cleanup();
createFiles();
createZipWithZip64Ext();
}
/**
* Delete files used by this test
* @throws IOException if an error occurs
*/
@AfterClass
public void cleanup() throws IOException {
System.out.println("In cleanup");
Files.deleteIfExists(Path.of(ZIP_FILE_NAME));
Files.deleteIfExists(Path.of(LARGE_FILE_NAME));
Files.deleteIfExists(Path.of(SMALL_FILE_NAME));
}
/**
* Create a Zip file that will result in the an Zip64 Extra (EXT) header
* being added to the CEN entry in order to find the LOC offset for
* SMALL_FILE_NAME.
*/
public static void createZipWithZip64Ext() {
System.out.println("Executing zip...");
List<String> commands = List.of("zip", "-0", ZIP_FILE_NAME,
LARGE_FILE_NAME, SMALL_FILE_NAME);
Result rc = run(new ProcessBuilder(commands));
rc.assertSuccess();
}
/**
* Navigate through the Zip file entries using Zip FS
* @throws IOException if an error occurs
*/
@Test
public void walkZipFSTest() throws IOException {
try (FileSystem fs =
FileSystems.newFileSystem(Paths.get(ZIP_FILE_NAME), Map.of("zipinfo-time", "False"))) {
for (Path root : fs.getRootDirectories()) {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes
attrs) throws IOException {
System.out.println(Files.readAttributes(file,
BasicFileAttributes.class).toString());
return FileVisitResult.CONTINUE;
}
});
}
}
}
/**
* Navigate through the Zip file entries using ZipFile
* @throws IOException if an error occurs
*/
@Test
public void walkZipFileTest() throws IOException {
try (ZipFile zip = new ZipFile(ZIP_FILE_NAME)) {
zip.stream().forEach(z -> System.out.printf("%s, %s, %s%n",
z.getName(), z.getMethod(), z.getLastModifiedTime()));
}
}
/**
* Create the files that will be added to the ZIP file
* @throws IOException if there is a problem creating the files
*/
private static void createFiles() throws IOException {
try (RandomAccessFile file = new RandomAccessFile(LARGE_FILE_NAME, "rw")
) {
System.out.printf("Creating %s%n", LARGE_FILE_NAME);
file.setLength(LARGE_FILE_SIZE);
System.out.printf("Creating %s%n", SMALL_FILE_NAME);
Files.writeString(Path.of(SMALL_FILE_NAME), "Hello");
}
}
/**
* Utility method to execute a ProcessBuilder command
* @param pb ProcessBuilder to execute
* @return The Result(s) from the ProcessBuilder execution
*/
private static Result run(ProcessBuilder pb) {
Process p;
System.out.printf("Running: %s%n", pb.command());
try {
p = pb.start();
} catch (IOException e) {
throw new RuntimeException(
format("Couldn't start process '%s'", pb.command()), e);
}
String output;
try {
output = toString(p.getInputStream(), p.getErrorStream());
} catch (IOException e) {
throw new RuntimeException(
format("Couldn't read process output '%s'", pb.command()), e);
}
try {
p.waitFor();
} catch (InterruptedException e) {
throw new RuntimeException(
format("Process hasn't finished '%s'", pb.command()), e);
}
return new Result(p.exitValue(), output);
}
/**
* Utility Method for combining the output from a ProcessBuilder invocation
* @param in1 ProccessBuilder.getInputStream
* @param in2 ProcessBuilder.getErrorStream
* @return The ProcessBuilder output
* @throws IOException if an error occurs
*/
static String toString(InputStream in1, InputStream in2) throws IOException {
try (ByteArrayOutputStream dst = new ByteArrayOutputStream();
InputStream concatenated = new SequenceInputStream(in1, in2)) {
concatenated.transferTo(dst);
return new String(dst.toByteArray(), StandardCharsets.UTF_8);
}
}
/**
* Utility class used to hold the results from a ProcessBuilder execution
*/
static class Result {
final int ec;
final String output;
private Result(int ec, String output) {
this.ec = ec;
this.output = output;
}
Result assertSuccess() {
assertTrue(ec == 0, "Expected ec 0, got: ", ec, " , output [", output, "]");
return this;
}
}
static void assertTrue(boolean cond, Object ... failedArgs) {
if (cond)
return;
StringBuilder sb = new StringBuilder();
for (Object o : failedArgs)
sb.append(o);
Assert.fail(sb.toString());
}
}