Author: msahyoun
Date: Sun Apr 12 18:39:13 2026
New Revision: 1932995

Log:
PDFBOX-6166: fixes after code review; adjust unit test; parts by Claude Sonnet

Modified:
   
pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStream.java
   
pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStreamTest.java

Modified: 
pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStream.java
==============================================================================
--- 
pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStream.java
       Sun Apr 12 18:21:22 2026        (r1932994)
+++ 
pdfbox/branches/3.0/io/src/main/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStream.java
       Sun Apr 12 18:39:13 2026        (r1932995)
@@ -94,9 +94,16 @@ public class NonSeekableRandomAccessRead
     @Override
     public void skip(int length) throws IOException
     {
-        for (int i = 0; i < length; i++)
+        byte[] skipBuffer = new byte[Math.min(length, BUFFER_SIZE)];
+        int remaining = length;
+        while (remaining > 0)
         {
-            read();
+            int bytesRead = read(skipBuffer, 0, Math.min(remaining, 
skipBuffer.length));
+            if (bytesRead == -1)
+            {
+                break;
+            }
+            remaining -= bytesRead;
         }
     }
 
@@ -137,6 +144,20 @@ public class NonSeekableRandomAccessRead
     public int read(byte[] b, int offset, int length) throws IOException
     {
         checkClosed();
+        // Parameter validation as defined in InputStream.read(byte[], int, 
int)
+        if (b == null)
+        {
+            throw new NullPointerException("buffer is null");
+        }
+        if (offset < 0 || length < 0 || offset + length > b.length)
+        {
+            throw new IndexOutOfBoundsException("buffer length=" + b.length + 
" offset=" + offset
+                    + " length=" + length);
+        }
+        if (length == 0)
+        {
+            return 0;
+        }
         if (isEOF())
         {
             return -1;
@@ -160,7 +181,7 @@ public class NonSeekableRandomAccessRead
                 break;
             }
         }
-        return numberOfBytesRead;
+        return numberOfBytesRead > 0 ? numberOfBytesRead : -1;
     }
 
     @Override
@@ -198,7 +219,8 @@ public class NonSeekableRandomAccessRead
     public int available() throws IOException
     {
         checkClosed();
-        return is.available();
+        int buffered = Math.max(0, bufferBytes[CURRENT] - 
currentBufferPointer);
+        return buffered + is.available();
     }
 
     private boolean fetch() throws IOException
@@ -242,7 +264,7 @@ public class NonSeekableRandomAccessRead
         catch (IOException exception)
         {
             // some data could be read -> don't throw an exception
-            LOG.warn("FlateFilter: premature end of stream due to a 
DataFormatException");
+            LOG.warn("premature end of stream, some data could be read ", 
exception);
             isEOF = true;
             throw exception;
         }
@@ -256,7 +278,7 @@ public class NonSeekableRandomAccessRead
     public long length() throws IOException
     {
         checkClosed();
-        return size;
+        return size + is.available();
     }
 
     @Override
@@ -267,8 +289,9 @@ public class NonSeekableRandomAccessRead
         {
             currentBufferPointer -= bytes;
             position -= bytes;
+            isEOF = false;
         }
-        else if (bufferBytes[LAST] > 0)
+        else if (bufferBytes[LAST] > 0 && (bytes - currentBufferPointer) <= 
bufferBytes[LAST])
         {
             // there is a former buffer
             int remainingBytesToRewind = bytes - currentBufferPointer;

Modified: 
pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStreamTest.java
==============================================================================
--- 
pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStreamTest.java
   Sun Apr 12 18:21:22 2026        (r1932994)
+++ 
pdfbox/branches/3.0/io/src/test/java/org/apache/pdfbox/io/NonSeekableRandomAccessReadInputStreamTest.java
   Sun Apr 12 18:39:13 2026        (r1932995)
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Asse
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayInputStream;
+import java.io.EOFException;
 import java.io.IOException;
 import java.io.OutputStream;
 
@@ -255,7 +256,7 @@ class NonSeekableRandomAccessReadInputSt
         ByteArrayInputStream bais = new ByteArrayInputStream(ba);
         try (RandomAccessRead rar = new 
NonSeekableRandomAccessReadInputStream(bais))
         {
-            assertEquals(0, rar.length()); // not really what I'd expect...
+            assertEquals(4096 * 2, rar.length());
             int len = rar.read(new byte[4096 + 1]);
             assertEquals(4096 * 2, rar.length());
             assertEquals(4096 + 1, len);
@@ -288,6 +289,172 @@ class NonSeekableRandomAccessReadInputSt
                     "read() should have thrown an IOException");
     }
 
+    /**
+     * Verify that all methods which require an open stream throw IOException 
after close().
+     */
+    @Test
+    void testClosedStreamMethods() throws IOException
+    {
+        ByteArrayInputStream bais = new ByteArrayInputStream(new byte[] { 1, 
2, 3 });
+        NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais);
+        rar.close();
+
+        Assertions.assertThrows(IOException.class, rar::read,
+                "read() on closed stream should throw IOException");
+        Assertions.assertThrows(IOException.class, () -> rar.read(new byte[1], 
0, 1),
+                "read(byte[], int, int) on closed stream should throw 
IOException");
+        Assertions.assertThrows(IOException.class, () -> rar.readFully(new 
byte[1], 0, 1),
+                "readFully() on closed stream should throw IOException");
+        Assertions.assertThrows(IOException.class, rar::getPosition,
+                "getPosition() on closed stream should throw IOException");
+        Assertions.assertThrows(IOException.class, rar::available,
+                "available() on closed stream should throw IOException");
+        Assertions.assertThrows(IOException.class, rar::length,
+                "length() on closed stream should throw IOException");
+        Assertions.assertThrows(IOException.class, rar::isEOF,
+                "isEOF() on closed stream should throw IOException");
+    }
+
+    /**
+     * Verify parameter validation in read(byte[], int, int) as required by 
the InputStream contract.
+     */
+    @Test
+    void testReadBytesParameterValidation() throws IOException
+    {
+        byte[] inputValues = { 0, 1, 2, 3, 4 };
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            // null buffer must throw NullPointerException
+            Assertions.assertThrows(NullPointerException.class, () -> 
rar.read(null, 0, 1),
+                    "null buffer should throw NullPointerException");
+
+            byte[] buf = new byte[4];
+
+            // negative offset must throw IndexOutOfBoundsException
+            Assertions.assertThrows(IndexOutOfBoundsException.class, () -> 
rar.read(buf, -1, 2),
+                    "negative offset should throw IndexOutOfBoundsException");
+
+            // negative length must throw IndexOutOfBoundsException
+            Assertions.assertThrows(IndexOutOfBoundsException.class, () -> 
rar.read(buf, 0, -1),
+                    "negative length should throw IndexOutOfBoundsException");
+
+            // offset + length beyond buffer end must throw 
IndexOutOfBoundsException
+            Assertions.assertThrows(IndexOutOfBoundsException.class, () -> 
rar.read(buf, 2, 4),
+                    "offset + length > buf.length should throw 
IndexOutOfBoundsException");
+
+            // length == 0 must return 0 immediately without advancing position
+            assertEquals(0, rar.read(buf, 0, 0));
+            assertEquals(0, rar.getPosition());
+        }
+    }
+
+    /**
+     * Verify that readFully() reads exactly the requested number of bytes 
across a buffer boundary.
+     */
+    @Test
+    void testReadFully() throws IOException
+    {
+        byte[] inputValues = new byte[10];
+        for (int i = 0; i < inputValues.length; i++)
+        {
+            inputValues[i] = (byte) i;
+        }
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            byte[] buf = new byte[10];
+            rar.readFully(buf, 0, 10);
+            for (int i = 0; i < 10; i++)
+            {
+                assertEquals(i, buf[i]);
+            }
+            assertEquals(10, rar.getPosition());
+        }
+    }
+
+    /**
+     * Verify that readFully() throws EOFException when the stream ends before 
the requested
+     * number of bytes are available.
+     */
+    @Test
+    void testReadFullyEOF() throws IOException
+    {
+        byte[] inputValues = { 0, 1, 2 };
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            Assertions.assertThrows(EOFException.class, () -> 
rar.readFully(new byte[10], 0, 10),
+                    "readFully() should throw EOFException when stream ends 
before length bytes");
+        }
+    }
+
+    /**
+     * Verify that skip() silently stops at EOF without throwing an exception.
+     */
+    @Test
+    void testSkipPastEOF() throws IOException
+    {
+        byte[] inputValues = { 0, 1, 2, 3, 4 };
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            // skipping far beyond the end of the stream should not throw
+            rar.skip(100);
+            assertEquals(5, rar.getPosition());
+            assertTrue(rar.isEOF());
+        }
+    }
+
+    /**
+     * Verify that available() accounts for bytes buffered internally as well 
as bytes remaining
+     * in the underlying stream, and returns 0 at EOF.
+     */
+    @Test
+    void testAvailable() throws IOException
+    {
+        byte[] inputValues = new byte[10];
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            // before any read, available() reflects is.available() since 
nothing is buffered yet
+            assertEquals(10, rar.available());
+
+            // read one byte: the fetch pulls all 10 bytes into the internal 
buffer,
+            // so available = 9 buffered + 0 remaining in the underlying stream
+            rar.read();
+            assertEquals(9, rar.available());
+
+            // consume all remaining bytes
+            while (rar.read() != -1) {}
+            assertEquals(0, rar.available());
+        }
+    }
+
+    /**
+     * Verify that length() returns the exact total after the stream is fully 
consumed,
+     * at which point size holds the true count and is.available() is 0.
+     */
+    @Test
+    void testLengthAfterFullConsumption() throws IOException
+    {
+        byte[] inputValues = new byte[100];
+        ByteArrayInputStream bais = new ByteArrayInputStream(inputValues);
+        try (NonSeekableRandomAccessReadInputStream rar =
+                new NonSeekableRandomAccessReadInputStream(bais))
+        {
+            while (rar.read() != -1) {}
+            assertTrue(rar.isEOF());
+            assertEquals(100, rar.length());
+        }
+    }
+
     private byte[] createRandomData()
     {
         final long seed = new Random().nextLong();

Reply via email to