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&lt;Path, 
R&gt;).</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()));
         }
     }
 }

Reply via email to