Skip to content

Commit 3647d98

Browse files
Alexey PavlyutkinYuri Nesterenko
authored andcommitted
8190753: (zipfs): Accessing a large entry (> 2^31 bytes) leads to a negative initial size for ByteArrayOutputStream
Reviewed-by: phh, andrew Backport-of: 8a9cda2d84513ab49a54e1d2a7b530f0bae05c61
1 parent b249159 commit 3647d98

File tree

3 files changed

+364
-5
lines changed

3 files changed

+364
-5
lines changed

jdk/src/share/demo/nio/zipfs/src/com/sun/nio/zipfs/ZipFileSystem.java

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
* this sample code.
3838
*/
3939

40-
4140
package com.sun.nio.zipfs;
4241

4342
import java.io.BufferedOutputStream;
@@ -94,6 +93,10 @@ public class ZipFileSystem extends FileSystem {
9493
private static final boolean isWindows =
9594
System.getProperty("os.name").startsWith("Windows");
9695

96+
// a threshold, in bytes, to decide whether to create a temp file
97+
// for outputstream of a zip entry
98+
private final int tempFileCreationThreshold = 10 * 1024 * 1024; // 10 MB
99+
97100
ZipFileSystem(ZipFileSystemProvider provider,
98101
Path zfpath,
99102
Map<String, ?> env)
@@ -1417,15 +1420,120 @@ private OutputStream getOutputStream(Entry e) throws IOException {
14171420
if (zc.isUTF8())
14181421
e.flag |= FLAG_EFS;
14191422
OutputStream os;
1420-
if (useTempFile) {
1423+
if (useTempFile || e.size >= tempFileCreationThreshold) {
14211424
e.file = getTempPathForEntry(null);
14221425
os = Files.newOutputStream(e.file, WRITE);
14231426
} else {
1424-
os = new ByteArrayOutputStream((e.size > 0)? (int)e.size : 8192);
1427+
os = new FileRolloverOutputStream(e);
14251428
}
14261429
return new EntryOutputStream(e, os);
14271430
}
14281431

1432+
// A wrapper around the ByteArrayOutputStream. This FileRolloverOutputStream
1433+
// uses a threshold size to decide if the contents being written need to be
1434+
// rolled over into a temporary file. Until the threshold is reached, writes
1435+
// on this outputstream just write it to the internal in-memory byte array
1436+
// held by the ByteArrayOutputStream. Once the threshold is reached, the
1437+
// write operation on this outputstream first (and only once) creates a temporary file
1438+
// and transfers the data that has so far been written in the internal
1439+
// byte array, to that newly created file. The temp file is then opened
1440+
// in append mode and any subsequent writes, including the one which triggered
1441+
// the temporary file creation, will be written to the file.
1442+
// Implementation note: the "write" and the "close" methods of this implementation
1443+
// aren't "synchronized" because this FileRolloverOutputStream gets called
1444+
// only from either DeflatingEntryOutputStream or EntryOutputStream, both of which
1445+
// already have the necessary "synchronized" before calling these methods.
1446+
private class FileRolloverOutputStream extends OutputStream {
1447+
private ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
1448+
private final Entry entry;
1449+
private OutputStream tmpFileOS;
1450+
private long totalWritten = 0;
1451+
1452+
private FileRolloverOutputStream(final Entry e) {
1453+
this.entry = e;
1454+
}
1455+
1456+
@Override
1457+
public void write(final int b) throws IOException {
1458+
if (tmpFileOS != null) {
1459+
// already rolled over, write to the file that has been created previously
1460+
writeToFile(b);
1461+
return;
1462+
}
1463+
if (totalWritten + 1 < tempFileCreationThreshold) {
1464+
// write to our in-memory byte array
1465+
baos.write(b);
1466+
totalWritten++;
1467+
return;
1468+
}
1469+
// rollover into a file
1470+
transferToFile();
1471+
writeToFile(b);
1472+
}
1473+
1474+
@Override
1475+
public void write(final byte[] b) throws IOException {
1476+
write(b, 0, b.length);
1477+
}
1478+
1479+
@Override
1480+
public void write(final byte[] b, final int off, final int len) throws IOException {
1481+
if (tmpFileOS != null) {
1482+
// already rolled over, write to the file that has been created previously
1483+
writeToFile(b, off, len);
1484+
return;
1485+
}
1486+
if (totalWritten + len < tempFileCreationThreshold) {
1487+
// write to our in-memory byte array
1488+
baos.write(b, off, len);
1489+
totalWritten += len;
1490+
return;
1491+
}
1492+
// rollover into a file
1493+
transferToFile();
1494+
writeToFile(b, off, len);
1495+
}
1496+
1497+
@Override
1498+
public void flush() throws IOException {
1499+
if (tmpFileOS != null) {
1500+
tmpFileOS.flush();
1501+
}
1502+
}
1503+
1504+
@Override
1505+
public void close() throws IOException {
1506+
baos = null;
1507+
if (tmpFileOS != null) {
1508+
tmpFileOS.close();
1509+
}
1510+
}
1511+
1512+
private void writeToFile(int b) throws IOException {
1513+
tmpFileOS.write(b);
1514+
totalWritten++;
1515+
}
1516+
1517+
private void writeToFile(byte[] b, int off, int len) throws IOException {
1518+
tmpFileOS.write(b, off, len);
1519+
totalWritten += len;
1520+
}
1521+
1522+
private void transferToFile() throws IOException {
1523+
// create a tempfile
1524+
entry.file = getTempPathForEntry(null);
1525+
tmpFileOS = new BufferedOutputStream(Files.newOutputStream(entry.file));
1526+
// transfer the already written data from the byte array buffer into this tempfile
1527+
baos.writeTo(tmpFileOS);
1528+
// release the underlying byte array
1529+
baos = null;
1530+
}
1531+
1532+
private byte[] toByteArray() {
1533+
return baos == null ? null : baos.toByteArray();
1534+
}
1535+
}
1536+
14291537
private InputStream getInputStream(Entry e)
14301538
throws IOException
14311539
{
@@ -1644,8 +1752,12 @@ public synchronized void close() throws IOException {
16441752
throw new ZipException("invalid compression method");
16451753
}
16461754
//crc.reset();
1647-
if (out instanceof ByteArrayOutputStream)
1648-
e.bytes = ((ByteArrayOutputStream)out).toByteArray();
1755+
if (out instanceof FileRolloverOutputStream) {
1756+
FileRolloverOutputStream fros = (FileRolloverOutputStream) out;
1757+
if (fros.tmpFileOS == null) {
1758+
e.bytes = fros.toByteArray();
1759+
}
1760+
}
16491761

16501762
if (e.type == Entry.FILECH) {
16511763
releaseDeflater(def);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*
23+
*/
24+
25+
import org.testng.annotations.AfterMethod;
26+
import org.testng.annotations.BeforeMethod;
27+
import org.testng.annotations.Test;
28+
29+
import java.io.IOException;
30+
import java.io.OutputStream;
31+
import java.nio.file.FileSystem;
32+
import java.nio.file.FileSystems;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
import java.nio.file.Paths;
36+
import java.util.Collections;
37+
import java.util.Random;
38+
import java.util.concurrent.TimeUnit;
39+
import java.net.URI;
40+
41+
42+
/**
43+
* @test
44+
* @bug 8190753 8011146
45+
* @summary Verify that using zip filesystem for opening an outputstream for a zip entry whose
46+
* compressed size is large, doesn't run into "Negative initial size" exception
47+
* @run testng/manual/othervm LargeCompressedEntrySizeTest
48+
*/
49+
public class LargeCompressedEntrySizeTest {
50+
51+
private static final String LARGE_FILE_NAME = "LargeZipEntry.txt";
52+
private static final String ZIP_FILE_NAME = "8190753-test-compressed-size.zip";
53+
54+
@BeforeMethod
55+
public void setUp() throws IOException {
56+
deleteFiles();
57+
}
58+
59+
@AfterMethod
60+
public void tearDown() throws IOException {
61+
deleteFiles();
62+
}
63+
64+
/**
65+
* Delete the files created for use by the test
66+
*
67+
* @throws IOException if an error occurs deleting the files
68+
*/
69+
private static void deleteFiles() throws IOException {
70+
Files.deleteIfExists(Paths.get(ZIP_FILE_NAME));
71+
}
72+
73+
74+
/**
75+
* Using zip filesystem, creates a zip file and writes out a zip entry whose compressed size is
76+
* expected to be greater than 2gb.
77+
*/
78+
@Test
79+
public void testLargeCompressedSizeWithZipFS() throws Exception {
80+
final Path zipFile = Paths.get(ZIP_FILE_NAME);
81+
final URI uri = URI.create("jar:" + zipFile.toUri());
82+
final long largeEntrySize = 6L * 1024L * 1024L * 1024L; // large value which exceeds Integer.MAX_VALUE
83+
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.singletonMap("create", "true"))) {
84+
try (OutputStream os = Files.newOutputStream(fs.getPath(LARGE_FILE_NAME))) {
85+
long remaining = largeEntrySize;
86+
// create a chunk of random bytes which we keep writing out
87+
final int chunkSize = 102400;
88+
final byte[] chunk = new byte[chunkSize];
89+
new Random().nextBytes(chunk);
90+
final long start = System.currentTimeMillis();
91+
for (long l = 0; l < largeEntrySize; l += chunkSize) {
92+
final int numToWrite = (int) Math.min(remaining, chunkSize);
93+
os.write(chunk, 0, numToWrite);
94+
remaining -= numToWrite;
95+
}
96+
System.out.println("Took " + TimeUnit.SECONDS.toSeconds(System.currentTimeMillis() - start)
97+
+ " seconds to generate entry of size " + largeEntrySize);
98+
}
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)