This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
commit fb7d4b109124a5f0b38b3ee66df77c1cf2ebb3ae Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Wed Feb 1 09:32:04 2023 -0500 [IO-786] Add UnsynchronizedBufferedInputStream --- src/changes/changes.xml | 3 + .../input/UnsynchronizedBufferedInputStream.java | 375 ++++++++++++++++ .../UnsynchronizedBufferedInputStreamTest.java | 484 +++++++++++++++++++++ 3 files changed, 862 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 412f94ab..0c0a1c7e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -460,6 +460,9 @@ The <action> type attribute can be add,update,fix,remove. <action issue="IO-786" dev="ggregory" type="add" due-to="Gary Gregory"> Add UnsynchronizedFilterInputStream. </action> + <action issue="IO-786" dev="ggregory" type="add" due-to="Gary Gregory, Benoit Tellier"> + Add UnsynchronizedBufferedInputStream. + </action> <!-- UPDATE --> <action dev="kinow" type="update" due-to="Dependabot, Gary Gregory"> Bump actions/cache from 2.1.6 to 3.0.10 #307, #337, #393. diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java new file mode 100644 index 00000000..2352d9eb --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.input; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; + +/** + * An unsynchronized version of {@link BufferedInputStream}, not thread-safe. + * <p> + * Wraps an existing {@link InputStream} and <em>buffers</em> the input. Expensive interaction with the underlying input stream is minimized, since most + * (smaller) requests can be satisfied by accessing the buffer alone. The drawback is that some extra space is required to hold the buffer and that copying + * takes place when filling that buffer, but this is usually outweighed by the performance benefits. + * </p> + * <p> + * A typical application pattern for the class looks like this: + * </p> + * + * <pre> + * UnsynchronizedBufferedInputStream buf = new UnsynchronizedBufferedInputStream(new FileInputStream("file.java")); + * </pre> + * <p> + * Provenance: Apache Harmony and modified. + * </p> + * + * @see BufferedInputStream + * @since 2.12.0 + */ +//@NotThreadSafe +public class UnsynchronizedBufferedInputStream extends UnsynchronizedFilterInputStream { + /** + * The buffer containing the current bytes read from the target InputStream. + */ + protected volatile byte[] buf; + + /** + * The total number of bytes inside the byte array {@code buf}. + */ + protected int count; + + /** + * The current limit, which when passed, invalidates the current mark. + */ + protected int marklimit; + + /** + * The currently marked position. -1 indicates no mark has been set or the mark has been invalidated. + */ + protected int markpos = IOUtils.EOF; + + /** + * The current position within the byte array {@code buf}. + */ + protected int pos; + + /** + * Constructs a new {@code BufferedInputStream} on the {@link InputStream} {@code in}. The default buffer size (8 KB) is allocated and all reads can now be + * filtered through this stream. + * + * @param in the InputStream the buffer reads from. + */ + public UnsynchronizedBufferedInputStream(final InputStream in) { + super(in); + buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; + } + + /** + * Constructs a new {@code BufferedInputStream} on the {@link InputStream} {@code in}. The buffer size is specified by the parameter {@code size} and all + * reads are now filtered through this stream. + * + * @param in the input stream the buffer reads from. + * @param size the size of buffer to allocate. + * @throws IllegalArgumentException if {@code size < 0}. + */ + public UnsynchronizedBufferedInputStream(final InputStream in, final int size) { + super(in); + if (size <= 0) { + throw new IllegalArgumentException("Size must be > 0"); + } + buf = new byte[size]; + } + + /** + * Returns the number of bytes that are available before this stream will block. This method returns the number of bytes available in the buffer plus those + * available in the source stream. + * + * @return the number of bytes available before blocking. + * @throws IOException if this stream is closed. + */ + @Override + public int available() throws IOException { + final InputStream localIn = in; // 'in' could be invalidated by close() + if (buf == null || localIn == null) { + throw new IOException("Stream is closed"); + } + return count - pos + localIn.available(); + } + + /** + * Closes this stream. The source stream is closed and any resources associated with it are released. + * + * @throws IOException if an error occurs while closing this stream. + */ + @Override + public void close() throws IOException { + buf = null; + final InputStream localIn = in; + in = null; + if (localIn != null) { + localIn.close(); + } + } + + private int fillbuf(final InputStream localIn, byte[] localBuf) throws IOException { + if (markpos == IOUtils.EOF || pos - markpos >= marklimit) { + /* Mark position not set or exceeded readlimit */ + final int result = localIn.read(localBuf); + if (result > 0) { + markpos = IOUtils.EOF; + pos = 0; + count = result; + } + return result; + } + if (markpos == 0 && marklimit > localBuf.length) { + /* Increase buffer size to accommodate the readlimit */ + int newLength = localBuf.length * 2; + if (newLength > marklimit) { + newLength = marklimit; + } + final byte[] newbuf = new byte[newLength]; + System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length); + // Reassign buf, which will invalidate any local references + // FIXME: what if buf was null? + localBuf = buf = newbuf; + } else if (markpos > 0) { + System.arraycopy(localBuf, markpos, localBuf, 0, localBuf.length - markpos); + } + /* Set the new position and mark position */ + pos -= markpos; + count = markpos = 0; + final int bytesread = localIn.read(localBuf, pos, localBuf.length - pos); + count = bytesread <= 0 ? pos : pos + bytesread; + return bytesread; + } + + /** + * Sets a mark position in this stream. The parameter {@code readlimit} indicates how many bytes can be read before a mark is invalidated. Calling + * {@code reset()} will reposition the stream back to the marked position if {@code readlimit} has not been surpassed. The underlying buffer may be + * increased in size to allow {@code readlimit} number of bytes to be supported. + * + * @param readlimit the number of bytes that can be read before the mark is invalidated. + * @see #reset() + */ + @Override + public void mark(final int readlimit) { + marklimit = readlimit; + markpos = pos; + } + + /** + * Indicates whether {@code BufferedInputStream} supports the {@code mark()} and {@code reset()} methods. + * + * @return {@code true} for BufferedInputStreams. + * @see #mark(int) + * @see #reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Reads a single byte from this stream and returns it as an integer in the range from 0 to 255. Returns -1 if the end of the source string has been + * reached. If the internal buffer does not contain any available bytes then it is filled from the source stream and the first byte is returned. + * + * @return the byte read or -1 if the end of the source stream has been reached. + * @throws IOException if this stream is closed or another IOException occurs. + */ + @Override + public int read() throws IOException { + // Use local refs since buf and in may be invalidated by an + // unsynchronized close() + byte[] localBuf = buf; + final InputStream localIn = in; + if (localBuf == null || localIn == null) { + throw new IOException("Stream is closed"); + } + + /* Are there buffered bytes available? */ + if (pos >= count && fillbuf(localIn, localBuf) == IOUtils.EOF) { + return IOUtils.EOF; /* no, fill buffer */ + } + // localBuf may have been invalidated by fillbuf + if (localBuf != buf) { + localBuf = buf; + if (localBuf == null) { + throw new IOException("Stream is closed"); + } + } + + /* Did filling the buffer fail with -1 (EOF)? */ + if (count - pos > 0) { + return localBuf[pos++] & 0xFF; + } + return IOUtils.EOF; + } + + /** + * Reads at most {@code length} bytes from this stream and stores them in byte array {@code buffer} starting at offset {@code offset}. Returns the number of + * bytes actually read or -1 if no bytes were read and the end of the stream was encountered. If all the buffered bytes have been used, a mark has not been + * set and the requested number of bytes is larger than the receiver's buffer size, this implementation bypasses the buffer and simply places the results + * directly into {@code buffer}. + * + * @param buffer the byte array in which to store the bytes read. + * @param offset the initial position in {@code buffer} to store the bytes read from this stream. + * @param length the maximum number of bytes to store in {@code buffer}. + * @return the number of bytes actually read or -1 if end of stream. + * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code length < 0}, or if {@code offset + length} is greater than the size of {@code buffer}. + * @throws IOException if the stream is already closed or another IOException occurs. + */ + @Override + public int read(final byte[] buffer, int offset, final int length) throws IOException { + // Use local ref since buf may be invalidated by an unsynchronized + // close() + byte[] localBuf = buf; + if (localBuf == null) { + throw new IOException("Stream is closed"); + } + // avoid int overflow + if (offset > buffer.length - length || offset < 0 || length < 0) { + throw new IndexOutOfBoundsException(); + } + if (length == 0) { + return 0; + } + final InputStream localIn = in; + if (localIn == null) { + throw new IOException("Stream is closed"); + } + + int required; + if (pos < count) { + /* There are bytes available in the buffer. */ + final int copylength = count - pos >= length ? length : count - pos; + System.arraycopy(localBuf, pos, buffer, offset, copylength); + pos += copylength; + if (copylength == length || localIn.available() == 0) { + return copylength; + } + offset += copylength; + required = length - copylength; + } else { + required = length; + } + + while (true) { + final int read; + /* + * If we're not marked and the required size is greater than the buffer, simply read the bytes directly bypassing the buffer. + */ + if (markpos == IOUtils.EOF && required >= localBuf.length) { + read = localIn.read(buffer, offset, required); + if (read == IOUtils.EOF) { + return required == length ? IOUtils.EOF : length - required; + } + } else { + if (fillbuf(localIn, localBuf) == IOUtils.EOF) { + return required == length ? IOUtils.EOF : length - required; + } + // localBuf may have been invalidated by fillbuf + if (localBuf != buf) { + localBuf = buf; + if (localBuf == null) { + throw new IOException("Stream is closed"); + } + } + + read = count - pos >= required ? required : count - pos; + System.arraycopy(localBuf, pos, buffer, offset, read); + pos += read; + } + required -= read; + if (required == 0) { + return length; + } + if (localIn.available() == 0) { + return length - required; + } + offset += read; + } + } + + /** + * Resets this stream to the last marked location. + * + * @throws IOException if this stream is closed, no mark has been set or the mark is no longer valid because more than {@code readlimit} bytes have been + * read since setting the mark. + * @see #mark(int) + */ + @Override + public void reset() throws IOException { + if (buf == null) { + throw new IOException("Stream is closed"); + } + if (IOUtils.EOF == markpos) { + throw new IOException("Mark has been invalidated"); + } + pos = markpos; + } + + /** + * Skips {@code amount} number of bytes in this stream. Subsequent {@code read()}'s will not return these bytes unless {@code reset()} is used. + * + * @param amount the number of bytes to skip. {@code skip} does nothing and returns 0 if {@code amount} is less than zero. + * @return the number of bytes actually skipped. + * @throws IOException if this stream is closed or another IOException occurs. + */ + @Override + public long skip(final long amount) throws IOException { + // Use local refs since buf and in may be invalidated by an + // unsynchronized close() + final byte[] localBuf = buf; + final InputStream localIn = in; + if (localBuf == null) { + throw new IOException("Stream is closed"); + } + if (amount < 1) { + return 0; + } + if (localIn == null) { + throw new IOException("Stream is closed"); + } + + if (count - pos >= amount) { + pos += amount; + return amount; + } + long read = count - pos; + pos = count; + + if (markpos != IOUtils.EOF && amount <= marklimit) { + if (fillbuf(localIn, localBuf) == IOUtils.EOF) { + return read; + } + if (count - pos >= amount - read) { + pos += amount - read; + return amount; + } + // Couldn't get all the bytes, skip what we read + read += count - pos; + pos = count; + return read; + } + return read + localIn.skip(amount - read); + } +} diff --git a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java new file mode 100644 index 00000000..547f73c4 --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java @@ -0,0 +1,484 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.input; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UnsynchronizedBufferedInputStream}. + * <p> + * Provenance: Apache Harmony and modified. + * </p> + */ +public class UnsynchronizedBufferedInputStreamTest { + + private static final int BUFFER_SIZE = 4096; + + static class MyUnsynchronizedBufferedInputStream extends UnsynchronizedBufferedInputStream { + static byte[] buf; + + MyUnsynchronizedBufferedInputStream(final InputStream is) { + super(is); + buf = super.buf; + } + + MyUnsynchronizedBufferedInputStream(final InputStream is, final int size) { + super(is, size); + buf = super.buf; + } + } + + Path fileName; + + private BufferedInputStream is; + + private InputStream isFile; + + byte[] ibuf = new byte[BUFFER_SIZE]; + + public static final String DATA = StringUtils.repeat("This is a test.", 500); + + /** + * Sets up the fixture, for example, open a network connection. This method is called before a test is executed. + * + * @throws IOException Thrown on test failure. + */ + @BeforeEach + protected void setUp() throws IOException { + fileName = Files.createTempFile(getClass().getSimpleName(), ".tst"); + Files.write(fileName, DATA.getBytes("UTF-8")); + + isFile = Files.newInputStream(fileName); + is = new BufferedInputStream(isFile); + } + + /** + * Tears down the fixture, for example, close a network connection. This method is called after a test is executed. + * + * @throws IOException Thrown on test failure. + */ + @AfterEach + protected void tearDown() throws IOException { + IOUtils.closeQuietly(is); + Files.deleteIfExists(fileName); + } + + /** + * Tests java.io.BufferedInputStream#available() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_available() throws IOException { + assertTrue(is.available() == DATA.length(), "Returned incorrect number of available bytes"); + + // Test that a closed stream throws an IOE for available() + final BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' })); + final int available = bis.available(); + bis.close(); + assertTrue(available != 0); + + assertThrows(IOException.class, () -> bis.available(), "Expected test to throw IOE."); + } + + /** + * Tests java.io.BufferedInputStream#close() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_close() throws IOException { + new BufferedInputStream(isFile).close(); + + // regression for HARMONY-667 + try (BufferedInputStream buf = new BufferedInputStream(null, 5)) { + // closes + } + + try (InputStream in = new InputStream() { + Object lock = new Object(); + + @Override + public void close() { + synchronized (lock) { + lock.notifyAll(); + } + } + + @Override + public int read() { + return 1; + } + + @Override + public int read(final byte[] buf, final int offset, final int length) { + synchronized (lock) { + try { + lock.wait(3000); + } catch (final InterruptedException e) { + // Ignore + } + } + return 1; + } + }) { + final BufferedInputStream bufin = new BufferedInputStream(in); + final Thread thread = new Thread(() -> { + try { + Thread.sleep(1000); + bufin.close(); + } catch (final Exception e) { + // Ignored + } + }); + thread.start(); + assertThrows(IOException.class, () -> bufin.read(new byte[100], 0, 99), "Should throw IOException"); + } + } + + /* + * Tests java.io.BufferedInputStream(InputStream) + */ + @Test + public void test_ConstructorLjava_io_InputStream() throws IOException { + try (BufferedInputStream str = new BufferedInputStream(null)) { + assertThrows(IOException.class, () -> str.read(), "Expected an IOException"); + } + } + + /* + * Tests java.io.BufferedInputStream(InputStream) + */ + @Test + public void test_ConstructorLjava_io_InputStreamI() throws IOException { + try (BufferedInputStream str = new BufferedInputStream(null, 1)) { + assertThrows(IOException.class, () -> str.read(), "Expected an IOException"); + } + + // Test for method java.io.BufferedInputStream(java.io.InputStream, int) + + // Create buffer with exact size of file + is = new BufferedInputStream(isFile, this.DATA.length()); + // Ensure buffer gets filled by evaluating one read + is.read(); + // Close underlying FileInputStream, all but 1 buffered bytes should + // still be available. + isFile.close(); + // Read the remaining buffered characters, no IOException should + // occur. + is.skip(this.DATA.length() - 2); + is.read(); + // is.read should now throw an exception because it will have to be filled. + assertThrows(IOException.class, () -> is.read()); + + // regression test for harmony-2407 + new MyUnsynchronizedBufferedInputStream(null); + assertNotNull(MyUnsynchronizedBufferedInputStream.buf); + MyUnsynchronizedBufferedInputStream.buf = null; + new MyUnsynchronizedBufferedInputStream(null, 100); + assertNotNull(MyUnsynchronizedBufferedInputStream.buf); + } + + /** + * Tests java.io.BufferedInputStream#mark(int) + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_markI() throws IOException { + final byte[] buf1 = new byte[100]; + final byte[] buf2 = new byte[100]; + is.skip(3000); + is.mark(1000); + is.read(buf1, 0, buf1.length); + is.reset(); + is.read(buf2, 0, buf2.length); + is.reset(); + assertTrue(new String(buf1, 0, buf1.length).equals(new String(buf2, 0, buf2.length)), "Failed to mark correct position"); + + byte[] bytes = new byte[256]; + for (int i = 0; i < 256; i++) { + bytes[i] = (byte) i; + } + InputStream in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12); + in.skip(6); + in.mark(14); + in.read(new byte[14], 0, 14); + in.reset(); + assertTrue(in.read() == 6 && in.read() == 7, "Wrong bytes"); + + in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12); + in.skip(6); + in.mark(8); + in.skip(7); + in.reset(); + assertTrue(in.read() == 6 && in.read() == 7, "Wrong bytes 2"); + + BufferedInputStream buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2); + buf.mark(3); + bytes = new byte[3]; + int result = buf.read(bytes); + assertEquals(3, result); + assertEquals(0, bytes[0], "Assert 0:"); + assertEquals(1, bytes[1], "Assert 1:"); + assertEquals(2, bytes[2], "Assert 2:"); + assertEquals(3, buf.read(), "Assert 3:"); + + buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2); + buf.mark(3); + bytes = new byte[4]; + result = buf.read(bytes); + assertEquals(4, result); + assertEquals(0, bytes[0], "Assert 4:"); + assertEquals(1, bytes[1], "Assert 5:"); + assertEquals(2, bytes[2], "Assert 6:"); + assertEquals(3, bytes[3], "Assert 7:"); + assertEquals(4, buf.read(), "Assert 8:"); + assertEquals(-1, buf.read(), "Assert 9:"); + + buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2); + buf.mark(Integer.MAX_VALUE); + buf.read(); + buf.close(); + } + + /** + * Tests java.io.BufferedInputStream#markSupported() + */ + @Test + public void test_markSupported() { + assertTrue(is.markSupported(), "markSupported returned incorrect value"); + } + + /** + * Tests java.io.BufferedInputStream#read() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_read() throws IOException { + final InputStreamReader isr = new InputStreamReader(is); + final int c = isr.read(); + assertTrue(c == DATA.charAt(0), "read returned incorrect char"); + + final byte[] bytes = new byte[256]; + for (int i = 0; i < 256; i++) { + bytes[i] = (byte) i; + } + final InputStream in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12); + assertEquals(0, in.read(), "Wrong initial byte"); // Fill the buffer + final byte[] buf = new byte[14]; + in.read(buf, 0, 14); // Read greater than the buffer + assertTrue(new String(buf, 0, 14).equals(new String(bytes, 1, 14)), "Wrong block read data"); + assertEquals(15, in.read(), "Wrong bytes"); // Check next byte + } + + /** + * Tests java.io.BufferedInputStream#read(byte[], int, int) + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_read$BII() throws IOException { + final byte[] buf1 = new byte[100]; + is.skip(3000); + is.mark(1000); + is.read(buf1, 0, buf1.length); + assertTrue(new String(buf1, 0, buf1.length).equals(DATA.substring(3000, 3100)), "Failed to read correct data"); + + try (BufferedInputStream bufin = new BufferedInputStream(new InputStream() { + int size = 2, pos = 0; + + byte[] contents = new byte[size]; + + @Override + public int available() { + return size - pos; + } + + @Override + public int read() throws IOException { + if (pos >= size) { + throw new IOException("Read past end of data"); + } + return contents[pos++]; + } + + @Override + public int read(final byte[] buf, final int off, final int len) throws IOException { + if (pos >= size) { + throw new IOException("Read past end of data"); + } + int toRead = len; + if (toRead > available()) { + toRead = available(); + } + System.arraycopy(contents, pos, buf, off, toRead); + pos += toRead; + return toRead; + } + })) { + bufin.read(); + final int result = bufin.read(new byte[2], 0, 2); + assertTrue(result == 1, () -> "Incorrect result: " + result); + } + } + + /** + * Tests java.io.BufferedInputStream#read(byte[], int, int) + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_read$BII_Exception() throws IOException { + final BufferedInputStream bis = new BufferedInputStream(null); + assertThrows(NullPointerException.class, () -> bis.read(null, -1, -1)); + + assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], -1, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], 1, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], 1, 1)); + + bis.close(); + + assertThrows(IOException.class, () -> bis.read(null, -1, -1)); + } + + /** + * Tests java.io.BufferedInputStream#reset() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_reset() throws IOException { + final byte[] buf1 = new byte[10]; + final byte[] buf2 = new byte[10]; + is.mark(2000); + is.read(buf1, 0, 10); + is.reset(); + is.read(buf2, 0, 10); + is.reset(); + assertTrue(new String(buf1, 0, buf1.length).equals(new String(buf2, 0, buf2.length)), "Reset failed"); + + final BufferedInputStream bIn = new BufferedInputStream(new ByteArrayInputStream("1234567890".getBytes())); + bIn.mark(10); + for (int i = 0; i < 11; i++) { + bIn.read(); + } + bIn.reset(); + } + + /** + * Tests java.io.BufferedInputStream#reset() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_reset_Exception() throws IOException { + final BufferedInputStream bis = new BufferedInputStream(null); + + // throws IOException with message "Mark has been invalidated" + assertThrows(IOException.class, () -> bis.reset()); + + // does not throw IOException + bis.mark(1); + bis.reset(); + + bis.close(); + + // throws IOException with message "stream is closed" + assertThrows(IOException.class, () -> bis.reset()); + } + + /** + * Tests java.io.BufferedInputStream#reset() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_reset_scenario1() throws IOException { + final byte[] input = "12345678900".getBytes(); + final BufferedInputStream buffis = new BufferedInputStream(new ByteArrayInputStream(input)); + buffis.read(); + buffis.mark(5); + buffis.skip(5); + buffis.reset(); + } + + /** + * Tests java.io.BufferedInputStream#reset() + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_reset_scenario2() throws IOException { + final byte[] input = "12345678900".getBytes(); + final BufferedInputStream buffis = new BufferedInputStream(new ByteArrayInputStream(input)); + buffis.mark(5); + buffis.skip(6); + buffis.reset(); + } + + /** + * Tests java.io.BufferedInputStream#skip(long) + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_skip_NullInputStream() throws IOException { + try (BufferedInputStream buf = new BufferedInputStream(null, 5)) { + assertEquals(0, buf.skip(0)); + } + } + + /** + * Tests java.io.BufferedInputStream#skip(long) + * + * @throws IOException Thrown on test failure. + */ + @Test + public void test_skipJ() throws IOException { + final byte[] buf1 = new byte[10]; + is.mark(2000); + is.skip(1000); + is.read(buf1, 0, buf1.length); + is.reset(); + assertTrue(new String(buf1, 0, buf1.length).equals(DATA.substring(1000, 1010)), "Failed to skip to correct position"); + + // regression for HARMONY-667 + try (BufferedInputStream buf = new BufferedInputStream(null, 5)) { + assertThrows(IOException.class, () -> buf.skip(10)); + } + } +}