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 9293f5f402e7c22d5526cc1df7aa180318ee640f Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Wed Dec 27 09:08:40 2023 -0500 [IO-818] NullInputStream breaks InputStream's read method contract --- src/changes/changes.xml | 1 + .../apache/commons/io/input/NullInputStream.java | 165 +++++++++------------ .../commons/io/input/NullInputStreamTest.java | 91 +++++++++++- 3 files changed, 157 insertions(+), 100 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 89022c6d..e457f803 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -81,6 +81,7 @@ The <action> type attribute can be add,update,fix,remove. <action dev="ggregory" type="fix" issue="IO-781" due-to="Marcono1234">Deprecate int CountingInputStream#getCount() in favor of long CountingInputStream#getByteCount().</action> <action dev="ggregory" type="fix" issue="IO-828" due-to="Elliotte Rusty Harold, Gary Gregory">Deprecate CountingInputStream.resetCount() in favor of resetByteCount().</action> <action dev="ggregory" type="fix" issue="IO-828" due-to="Gary Gregory">Deprecate CountingInputStream.getMaxLength() in favor of getMaxCount()).</action> + <action dev="ggregory" type="fix" issue="IO-818" due-to="Gary Gregory">NullInputStream breaks InputStream's read method contract.</action> <!-- Add --> <action dev="ggregory" type="add" due-to="Gary Gregory">Add and use PathUtils.getFileName(Path, Function<Path, R>).</action> <action dev="ggregory" type="add" due-to="Gary Gregory">Add and use PathUtils.getFileNameString().</action> diff --git a/src/main/java/org/apache/commons/io/input/NullInputStream.java b/src/main/java/org/apache/commons/io/input/NullInputStream.java index f7f0f594..31756bd3 100644 --- a/src/main/java/org/apache/commons/io/input/NullInputStream.java +++ b/src/main/java/org/apache/commons/io/input/NullInputStream.java @@ -23,27 +23,17 @@ import java.io.IOException; import java.io.InputStream; /** - * A functional, light weight {@link InputStream} that emulates - * a stream of a specified size. + * A functional, light weight {@link InputStream} that emulates a stream of a specified size. * <p> - * This implementation provides a light weight - * object for testing with an {@link InputStream} - * where the contents don't matter. + * This implementation provides a light weight object for testing with an {@link InputStream} where the contents don't matter. * </p> * <p> - * One use case would be for testing the handling of - * large {@link InputStream} as it can emulate that - * scenario without the overhead of actually processing - * large numbers of bytes - significantly speeding up - * test execution times. + * One use case would be for testing the handling of large {@link InputStream} as it can emulate that scenario without the overhead of actually processing large + * numbers of bytes - significantly speeding up test execution times. * </p> * <p> - * This implementation returns zero from the method that - * reads a byte and leaves the array unchanged in the read - * methods that are passed a byte array. - * If alternative data is required the {@code processByte()} and - * {@code processBytes()} methods can be implemented to generate - * data, for example: + * This implementation returns zero from the method that reads a byte and leaves the array unchanged in the read methods that are passed a byte array. If + * alternative data is required the {@code processByte()} and {@code processBytes()} methods can be implemented to generate data, for example: * </p> * * <pre> @@ -82,40 +72,34 @@ public class NullInputStream extends InputStream { private final boolean markSupported; /** - * Constructs an {@link InputStream} that emulates a size 0 stream - * which supports marking and does not throw EOFException. + * Constructs an {@link InputStream} that emulates a size 0 stream which supports marking and does not throw EOFException. * * @since 2.7 */ public NullInputStream() { - this(0, true, false); + this(0, true, false); } /** - * Constructs an {@link InputStream} that emulates a specified size - * which supports marking and does not throw EOFException. + * Constructs an {@link InputStream} that emulates a specified size which supports marking and does not throw EOFException. * * @param size The size of the input stream to emulate. */ public NullInputStream(final long size) { - this(size, true, false); + this(size, true, false); } /** - * Constructs an {@link InputStream} that emulates a specified - * size with option settings. + * Constructs an {@link InputStream} that emulates a specified size with option settings. * - * @param size The size of the input stream to emulate. - * @param markSupported Whether this instance will support - * the {@code mark()} functionality. - * @param throwEofException Whether this implementation - * will throw an {@link EOFException} or return -1 when the - * end of file is reached. + * @param size The size of the input stream to emulate. + * @param markSupported Whether this instance will support the {@code mark()} functionality. + * @param throwEofException Whether this implementation will throw an {@link EOFException} or return -1 when the end of file is reached. */ public NullInputStream(final long size, final boolean markSupported, final boolean throwEofException) { - this.size = size; - this.markSupported = markSupported; - this.throwEofException = throwEofException; + this.size = size; + this.markSupported = markSupported; + this.throwEofException = throwEofException; } /** @@ -136,8 +120,19 @@ public class NullInputStream extends InputStream { } /** - * Closes this input stream - resets the internal state to - * the initial values. + * Throws {@link EOFException} if {@code throwEofException} is enabled. + * + * @param message The {@link EOFException} message. + * @throws EOFException Thrown if {@code throwEofException} is enabled. + */ + private void checkThrowEof(final String message) throws EOFException { + if (throwEofException) { + throw new EOFException(message); + } + } + + /** + * Closes this input stream - resets the internal state to the initial values. * * @throws IOException If an error occurs. */ @@ -148,22 +143,6 @@ public class NullInputStream extends InputStream { mark = -1; } - /** - * Handles End of File. - * - * @return {@code -1} if {@code throwEofException} is - * set to {@code false} - * @throws EOFException if {@code throwEofException} is set - * to {@code true}. - */ - private int doEndOfFile() throws EOFException { - eof = true; - if (throwEofException) { - throw new EOFException(); - } - return EOF; - } - /** * Gets the current position. * @@ -182,11 +161,22 @@ public class NullInputStream extends InputStream { return size; } + /** + * Handles End of File. + * + * @return {@code -1} if {@code throwEofException} is set to {@code false} + * @throws EOFException if {@code throwEofException} is set to {@code true}. + */ + private int handleEof() throws EOFException { + eof = true; + checkThrowEof("handleEof()"); + return EOF; + } + /** * Marks the current position. * - * @param readLimit The number of bytes before this marked position - * is invalid. + * @param readLimit The number of bytes before this marked position is invalid. * @throws UnsupportedOperationException if mark is not supported. */ @Override @@ -209,7 +199,7 @@ public class NullInputStream extends InputStream { } /** - * Returns a byte value for the {@code read()} method. + * Returns a byte value for the {@code read()} method. * <p> * This implementation returns zero. * @@ -221,13 +211,12 @@ public class NullInputStream extends InputStream { } /** - * Processes the bytes for the {@code read(byte[], offset, length)} - * method. + * Processes the bytes for the {@code read(byte[], offset, length)} method. * <p> * This implementation leaves the byte array unchanged. * </p> * - * @param bytes The byte array + * @param bytes The byte array * @param offset The offset to start at. * @param length The number of bytes. */ @@ -238,20 +227,19 @@ public class NullInputStream extends InputStream { /** * Reads a byte. * - * @return Either The byte value returned by {@code processByte()} - * or {@code -1} if the end of file has been reached and - * {@code throwEofException} is set to {@code false}. - * @throws EOFException if the end of file is reached and - * {@code throwEofException} is set to {@code true}. - * @throws IOException if trying to read past the end of file. + * @return Either The byte value returned by {@code processByte()} or {@code -1} if the end of file has been reached and {@code throwEofException} is set to + * {@code false}. + * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. + * @throws IOException if trying to read past the end of file. */ @Override public int read() throws IOException { if (eof) { - throw new IOException("Read after end of file"); + checkThrowEof("read()"); + return EOF; } if (position == size) { - return doEndOfFile(); + return handleEof(); } position++; return processByte(); @@ -261,12 +249,9 @@ public class NullInputStream extends InputStream { * Reads some bytes into the specified array. * * @param bytes The byte array to read into - * @return The number of bytes read or {@code -1} - * if the end of file has been reached and - * {@code throwEofException} is set to {@code false}. - * @throws EOFException if the end of file is reached and - * {@code throwEofException} is set to {@code true}. - * @throws IOException if trying to read past the end of file. + * @return The number of bytes read or {@code -1} if the end of file has been reached and {@code throwEofException} is set to {@code false}. + * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. + * @throws IOException if trying to read past the end of file. */ @Override public int read(final byte[] bytes) throws IOException { @@ -276,23 +261,21 @@ public class NullInputStream extends InputStream { /** * Reads the specified number bytes into an array. * - * @param bytes The byte array to read into. + * @param bytes The byte array to read into. * @param offset The offset to start reading bytes into. * @param length The number of bytes to read. - * @return The number of bytes read or {@code -1} - * if the end of file has been reached and - * {@code throwEofException} is set to {@code false}. - * @throws EOFException if the end of file is reached and - * {@code throwEofException} is set to {@code true}. - * @throws IOException if trying to read past the end of file. + * @return The number of bytes read or {@code -1} if the end of file has been reached and {@code throwEofException} is set to {@code false}. + * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. + * @throws IOException if trying to read past the end of file. */ @Override public int read(final byte[] bytes, final int offset, final int length) throws IOException { if (eof) { - throw new IOException("Read after end of file"); + checkThrowEof("read(byte[], int, int)"); + return EOF; } if (position == size) { - return doEndOfFile(); + return handleEof(); } position += length; int returnLength = length; @@ -308,9 +291,7 @@ public class NullInputStream extends InputStream { * Resets the stream to the point when mark was last called. * * @throws UnsupportedOperationException if mark is not supported. - * @throws IOException If no position has been marked - * or the read limit has been exceeded since the last position was - * marked. + * @throws IOException If no position has been marked or the read limit has been exceeded since the last position was marked. */ @Override public synchronized void reset() throws IOException { @@ -321,9 +302,7 @@ public class NullInputStream extends InputStream { throw new IOException("No position has been marked"); } if (position > mark + readLimit) { - throw new IOException("Marked position [" + mark + - "] is no longer valid - passed the read limit [" + - readLimit + "]"); + throw new IOException("Marked position [" + mark + "] is no longer valid - passed the read limit [" + readLimit + "]"); } position = mark; eof = false; @@ -333,20 +312,18 @@ public class NullInputStream extends InputStream { * Skips a specified number of bytes. * * @param numberOfBytes The number of bytes to skip. - * @return The number of bytes skipped or {@code -1} - * if the end of file has been reached and - * {@code throwEofException} is set to {@code false}. - * @throws EOFException if the end of file is reached and - * {@code throwEofException} is set to {@code true}. - * @throws IOException if trying to read past the end of file. + * @return The number of bytes skipped or {@code -1} if the end of file has been reached and {@code throwEofException} is set to {@code false}. + * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. + * @throws IOException if trying to read past the end of file. */ @Override public long skip(final long numberOfBytes) throws IOException { if (eof) { - throw new IOException("Skip after end of file"); + checkThrowEof("skip(long)"); + return EOF; } if (position == size) { - return doEndOfFile(); + return handleEof(); } position += numberOfBytes; long returnLength = numberOfBytes; diff --git a/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java b/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java index da6460ea..fd264eec 100644 --- a/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.InputStream; import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.StringUtils; /** * Tests {@link NullInputStream}. @@ -33,6 +34,7 @@ import org.junit.jupiter.api.Test; public class NullInputStreamTest { private static final class TestNullInputStream extends NullInputStream { + public TestNullInputStream(final int size) { super(size); } @@ -56,7 +58,7 @@ public class NullInputStreamTest { } - // Use the same message as in java.io.InputStream.reset() in OpenJDK 8.0.275-1. + /** Use the same message as in java.io.InputStream.reset() in OpenJDK 8.0.275-1. */ private static final String MARK_RESET_NOT_SUPPORTED = "mark/reset not supported"; @Test @@ -135,8 +137,7 @@ public class NullInputStreamTest { assertEquals(0, input.available(), "Available after End of File"); // Test reading after the end of file - final IOException e = assertThrows(IOException.class, input::read); - assertEquals("Read after end of file", e.getMessage()); + assertEquals(-1, input.read(), "End of File"); // Close - should reset input.close(); @@ -167,8 +168,8 @@ public class NullInputStreamTest { assertEquals(-1, count3, "Read 3 (EOF)"); // Test reading after the end of file - final IOException e = assertThrows(IOException.class, () -> input.read(bytes)); - assertEquals("Read after end of file", e.getMessage()); + final int count4 = input.read(bytes); + assertEquals(-1, count4, "Read 4 (EOF)"); // reset by closing input.close(); @@ -183,6 +184,69 @@ public class NullInputStreamTest { } } + @Test + public void testReadByteArrayThrowAtEof() throws Exception { + final byte[] bytes = new byte[10]; + final InputStream input = new TestNullInputStream(15, true, true); + + // Read into array + final int count1 = input.read(bytes); + assertEquals(bytes.length, count1, "Read 1"); + for (int i = 0; i < count1; i++) { + assertEquals(i, bytes[i], "Check Bytes 1"); + } + + // Read into array + final int count2 = input.read(bytes); + assertEquals(5, count2, "Read 2"); + for (int i = 0; i < count2; i++) { + assertEquals(count1 + i, bytes[i], "Check Bytes 2"); + } + + // End of File + final IOException e1 = assertThrows(EOFException.class, () -> input.read(bytes)); + assertTrue(StringUtils.isNotBlank(e1.getMessage())); + + // Test reading after the end of file + final IOException e2 = assertThrows(EOFException.class, () -> input.read(bytes)); + assertTrue(StringUtils.isNotBlank(e2.getMessage())); + + // reset by closing + input.close(); + + // Read into array using offset & length + final int offset = 2; + final int lth = 4; + final int count5 = input.read(bytes, offset, lth); + assertEquals(lth, count5, "Read 5"); + for (int i = offset; i < lth; i++) { + assertEquals(i, bytes[i], "Check Bytes 2"); + } + } + + @Test + public void testReadThrowAtEof() throws Exception { + final int size = 5; + final InputStream input = new TestNullInputStream(size, true, true); + for (int i = 0; i < size; i++) { + assertEquals(size - i, input.available(), "Check Size [" + i + "]"); + assertEquals(i, input.read(), "Check Value [" + i + "]"); + } + assertEquals(0, input.available(), "Available after contents all read"); + + // Check available is zero after End of file + final IOException e1 = assertThrows(EOFException.class, input::read); + assertTrue(StringUtils.isNotBlank(e1.getMessage())); + + // Test reading after the end of file + final IOException e2 = assertThrows(EOFException.class, input::read); + assertTrue(StringUtils.isNotBlank(e1.getMessage())); + + // Close - should reset + input.close(); + assertEquals(size, input.available(), "Available after close"); + } + @Test public void testSkip() throws Exception { try (InputStream input = new TestNullInputStream(10, true, false)) { @@ -192,9 +256,24 @@ public class NullInputStreamTest { assertEquals(7, input.read(), "Read 3"); assertEquals(2, input.skip(5), "Skip 2"); // only 2 left to skip assertEquals(-1, input.skip(5), "Skip 3 (EOF)"); // End of file + assertEquals(-1, input.skip(5), "Skip 3 (EOF)"); // End of file + } + } + + @Test + public void testSkipThrowAtEof() throws Exception { + try (InputStream input = new TestNullInputStream(10, true, true)) { + assertEquals(0, input.read(), "Read 1"); + assertEquals(1, input.read(), "Read 2"); + assertEquals(5, input.skip(5), "Skip 1"); + assertEquals(7, input.read(), "Read 3"); + assertEquals(2, input.skip(5), "Skip 2"); // only 2 left to skip + // End of File + final IOException e1 = assertThrows(EOFException.class, () -> input.skip(5), "Skip 3 (EOF)"); + assertTrue(StringUtils.isNotBlank(e1.getMessage())); final IOException e = assertThrows(IOException.class, () -> input.skip(5), "Expected IOException for skipping after end of file"); - assertEquals("Skip after end of file", e.getMessage(), "Skip after EOF IOException message"); + assertTrue(StringUtils.isNotBlank(e1.getMessage())); } } }