diff --git a/src/java.base/share/classes/java/util/zip/InflaterOutputStream.java b/src/java.base/share/classes/java/util/zip/InflaterOutputStream.java index 02900c741d9..ebeaaac540d 100644 --- a/src/java.base/share/classes/java/util/zip/InflaterOutputStream.java +++ b/src/java.base/share/classes/java/util/zip/InflaterOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 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 @@ -236,16 +236,9 @@ public void write(byte[] b, int off, int len) throws IOException { // Fill the decompressor buffer with output data if (inf.needsInput()) { - int part; - - if (len < 1) { - break; - } - - part = (len < 512 ? len : 512); - inf.setInput(b, off, part); - off += part; - len -= part; + inf.setInput(b, off, len); + // Only use input buffer once. + len = 0; } // Decompress and write blocks of output data @@ -256,13 +249,14 @@ public void write(byte[] b, int off, int len) throws IOException { } } while (n > 0); - // Check the decompressor - if (inf.finished()) { - break; - } + // Check for missing dictionary first if (inf.needsDictionary()) { throw new ZipException("ZLIB dictionary missing"); } + // Check the decompressor + if (inf.finished() || (len == 0)/* no more input */) { + break; + } } } catch (DataFormatException ex) { // Improperly formatted compressed (ZIP) data diff --git a/test/jdk/java/util/zip/DeflateIn_InflateOut.java b/test/jdk/java/util/zip/DeflateIn_InflateOut.java index bce10c30b05..68ad0eaa036 100644 --- a/test/jdk/java/util/zip/DeflateIn_InflateOut.java +++ b/test/jdk/java/util/zip/DeflateIn_InflateOut.java @@ -41,12 +41,29 @@ public class DeflateIn_InflateOut { private static ByteArrayOutputStream baos; private static InflaterOutputStream ios; - private static void reset() { + private static Inflater reset(byte[] dict) { bais = new ByteArrayInputStream(data); - dis = new DeflaterInputStream(bais); + if (dict == null) { + dis = new DeflaterInputStream(bais); + } else { + Deflater def = new Deflater(); + def.setDictionary(dict); + dis = new DeflaterInputStream(bais, def); + } baos = new ByteArrayOutputStream(); - ios = new InflaterOutputStream(baos); + if (dict == null) { + ios = new InflaterOutputStream(baos); + return null; + } else { + Inflater inf = new Inflater(); + ios = new InflaterOutputStream(baos, inf); + return inf; + } + } + + private static void reset() { + reset(null); } /** Check byte arrays read/write. */ @@ -214,6 +231,44 @@ private static void SkipBytes() throws Throwable { check(numNotSkipped + numSkipBytes == numReadable); } + /** Check "needsDictionary()". */ + private static void NeedsDictionary() throws Throwable { + byte[] dict = {1, 2, 3, 4}; + Adler32 adler32 = new Adler32(); + adler32.update(dict); + long checksum = adler32.getValue(); + byte[] buf = new byte[512]; + + Inflater inf = reset(dict); + check(dis.available() == 1); + boolean dictSet = false; + for (;;) { + int len = dis.read(buf, 0, buf.length); + if (len < 0) { + break; + } else { + try { + ios.write(buf, 0, len); + if (dictSet == false) { + check(false, "Must throw ZipException without dictionary"); + return; + } + } catch (ZipException ze) { + check(dictSet == false, "Dictonary must be set only once"); + check(checksum == inf.getAdler(), "Incorrect dictionary"); + inf.setDictionary(dict); + // After setting the dictionary, we have to flush the + // InflaterOutputStream now in order to consume all the + // pending input data from the last, failed call to "write()". + ios.flush(); + dictSet = true; + } + } + } + check(dis.available() == 0); + ios.close(); + check(Arrays.equals(data, baos.toByteArray())); + } public static void realMain(String[] args) throws Throwable { new Random(new Date().getTime()).nextBytes(data); @@ -227,15 +282,24 @@ public static void realMain(String[] args) throws Throwable { ByteReadByteWrite(); SkipBytes(); + + NeedsDictionary(); } //--------------------- Infrastructure --------------------------- static volatile int passed = 0, failed = 0; static void pass() {passed++;} - static void fail() {failed++; Thread.dumpStack();} - static void fail(String msg) {System.out.println(msg); fail();} + static void fail() { fail(null); } + static void fail(String msg) { + failed++; + if (msg != null) { + System.err.println(msg); + } + Thread.dumpStack(); + } static void unexpected(Throwable t) {failed++; t.printStackTrace();} static void check(boolean cond) {if (cond) pass(); else fail();} + static void check(boolean cond, String msg) {if (cond) pass(); else fail(msg);} static void equal(Object x, Object y) { if (x == null ? y == null : x.equals(y)) pass(); else fail(x + " not equal to " + y);} diff --git a/test/micro/org/openjdk/bench/java/util/zip/Streams.java b/test/micro/org/openjdk/bench/java/util/zip/Streams.java new file mode 100644 index 00000000000..46d1b7822e8 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/zip/Streams.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2020, Amazon 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. + */ + +package org.openjdk.bench.java.util.zip; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.zip.Inflater; +import java.util.zip.InflaterOutputStream; +import java.util.zip.DeflaterOutputStream; + +/** + * Test the average execution time of "InflaterOutputStream.write()" depending + * on the size of the internal "InflaterOutputStream" byte buffer and the size + * of the compressed data input buffer passed to "write()". + * + * The size of the compressed data input buffer is controlled by the "size" + * parameter which runs from "512" to "65536". + * + * The size of the internal byte buffer is a multiple of "size" controlled by + * the "scale" paramter which runs from "1" to "8". + * + * For peak perfomance the internal buffer should be big enough to hold all + * the data decompressed from the input buffer. This of course depends on + * the compression rate of the input data. E.g. if the compression rate of + * the compressed input data is 4 (i.e. the original input data was compressed + * to 1/4 of its original size) the internal buffer should be four times bigger + * than the size of the compressed data buffer passed to "write()" because in + * that case one single call to the native zlib "inflate()" method is sufficent + * to decompress all data and store it in the output buffer from where it can + * be written to the output stream with one single call to the output streams + * "write()" method. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Thread) +public class Streams { + + private FileInputStream in; + private FileOutputStream out; + @Param({"512", "1024", "2048", "4096", "8192", "16384", "32768", "65536"}) + private int size; + @Param({"1", "2", "4", "8"}) + private int scale; + private byte[] buf; + + private static byte[] data = new byte[1024 * 1024]; + + @Setup(Level.Trial) + public void beforeRun() throws IOException { + // The reason for this whole dance is to programmatically create a one + // megabyte file which can be compressed by factor ~6. This will give + // us good results for the various scale factors (i.e. the relation + // between the deflated input buffer and the inflated output buffer). + // We achieve the desired compression factor by creating a 64 byte + // array of random data and than fill the final 1mb file with random + // 8-byte substrings of these 64 random bytes. This vaguely mimics + // a language with 8 character words over a set of 64 different characters. + final int characters = 64; + final int wordLength = 8; + buf = new byte[characters]; + Random r = new Random(123456789); + r.nextBytes(buf); + for (int i = 0; i < data.length / wordLength; i++) { + System.arraycopy(buf, r.nextInt(characters - wordLength), data, i * wordLength, wordLength); + } + ByteArrayInputStream bais = new ByteArrayInputStream(data); + + File deflated = File.createTempFile("inflaterOutputStreamWrite", ".deflated"); + deflated.deleteOnExit(); + FileOutputStream fout = new FileOutputStream(deflated); + DeflaterOutputStream defout = new DeflaterOutputStream(fout); + bais.transferTo(defout); + // We need to close the DeflaterOutputStream in order to flush all the + // compressed data in the Deflater and the underlying FileOutputStream. + defout.close(); + in = new FileInputStream(deflated); + File inflated = File.createTempFile("inflaterOutputStreamWrite", ".inflated"); + inflated.deleteOnExit(); + out = new FileOutputStream(inflated); + } + + @Setup(Level.Iteration) + public void beforeIteration() throws IOException { + in.getChannel().position(0); + out.getChannel().position(0); + buf = new byte[size]; + } + + @Benchmark + public void inflaterOutputStreamWrite() throws IOException { + in.getChannel().position(0); + out.getChannel().position(0); + InflaterOutputStream inflate = new InflaterOutputStream(out, new Inflater(), scale * size); + int len; + // buf.length == size + while ((len = in.read(buf)) != -1) { + inflate.write(buf, 0, len); + } + inflate.finish(); + } +}