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();