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


The following commit(s) were added to refs/heads/master by this push:
     new 208887b4e ByteArraySeekableByteChannel.position|truncate(long) 
shouldn't throw an IllegalArgumentException for a new positive position that's 
too large (#817)
208887b4e is described below

commit 208887b4e351a3e2ef3cec77238b0677136fb051
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Dec 8 16:57:19 2025 -0500

    ByteArraySeekableByteChannel.position|truncate(long) shouldn't throw an 
IllegalArgumentException for a new positive position that's too large (#817)
    
    * [IO-856] Try test on all OSs for GitHub CI
    
    * ByteArraySeekableByteChannel.position|truncate(long) shouldn't throw an
    IllegalArgumentException for a new positive position that's too large
    
    * Throw IOException instead of OutOfMemoryError
    
    * Refactor internals for safer and less type casting
---
 .../io/channels/ByteArraySeekableByteChannel.java  | 59 +++++++++++++---------
 .../channels/AbstractSeekableByteChannelTest.java  | 30 ++++++++++-
 .../ByteArraySeekableByteChannelCompressTest.java  | 31 ++++++++++--
 .../channels/ByteArraySeekableByteChannelTest.java | 16 ++++++
 4 files changed, 106 insertions(+), 30 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java
 
b/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java
index 4135a815e..2cd800920 100644
--- 
a/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java
+++ 
b/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java
@@ -68,7 +68,7 @@ public static ByteArraySeekableByteChannel wrap(final byte[] 
bytes) {
 
     private byte[] data;
     private volatile boolean closed;
-    private int position;
+    private long position;
     private int size;
     private final ReentrantLock lock = new ReentrantLock();
 
@@ -126,11 +126,10 @@ private void checkOpen() throws ClosedChannelException {
         }
     }
 
-    private int checkRange(final long newSize, final String method) {
-        if (newSize < 0L || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) {
-            throw new IllegalArgumentException(String.format("%s must be in 
range [0..%,d]: %,d", method, IOUtils.SOFT_MAX_ARRAY_LENGTH, newSize));
+    private void checkRange(final long newSize, final String method) {
+        if (newSize < 0L) {
+            throw new IllegalArgumentException(String.format("%s must be 
positive: %,d", method, newSize));
         }
-        return (int) newSize;
     }
 
     @Override
@@ -166,10 +165,10 @@ public long position() throws ClosedChannelException {
     @Override
     public SeekableByteChannel position(final long newPosition) throws 
IOException {
         checkOpen();
-        final int intPos = checkRange(newPosition, "position()");
+        checkRange(newPosition, "position()");
         lock.lock();
         try {
-            position = intPos;
+            position = newPosition;
         } finally {
             lock.unlock();
         }
@@ -181,15 +180,18 @@ public int read(final ByteBuffer buf) throws IOException {
         checkOpen();
         lock.lock();
         try {
+            if (position > Integer.MAX_VALUE) {
+                return IOUtils.EOF;
+            }
             int wanted = buf.remaining();
-            final int possible = size - position;
+            final int possible = size - (int) position;
             if (possible <= 0) {
                 return IOUtils.EOF;
             }
             if (wanted > possible) {
                 wanted = possible;
             }
-            buf.put(data, position, wanted);
+            buf.put(data, (int) position, wanted);
             position += wanted;
             return wanted;
         } finally {
@@ -238,14 +240,14 @@ public byte[] toByteArray() {
     @Override
     public SeekableByteChannel truncate(final long newSize) throws 
ClosedChannelException {
         checkOpen();
-        final int intSize = checkRange(newSize, "truncate()");
+        checkRange(newSize, "truncate()");
         lock.lock();
         try {
-            if (size > intSize) {
-                size = intSize;
+            if (size > newSize) {
+                size = (int) newSize;
             }
-            if (position > intSize) {
-                position = intSize;
+            if (position > newSize) {
+                position = newSize;
             }
         } finally {
             lock.unlock();
@@ -256,21 +258,28 @@ public SeekableByteChannel truncate(final long newSize) 
throws ClosedChannelExce
     @Override
     public int write(final ByteBuffer b) throws IOException {
         checkOpen();
+        if (position > Integer.MAX_VALUE) {
+            throw new IOException("position > Integer.MAX_VALUE");
+        }
         lock.lock();
         try {
             final int wanted = b.remaining();
-            final int possibleWithoutResize = Math.max(0, size - position);
-            if (wanted > possibleWithoutResize) {
-                final int newSize = position + wanted;
-                if (newSize < 0 || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { 
// overflow
-                    throw new OutOfMemoryError("required array size " + 
Integer.toUnsignedString(newSize) + " too large");
-                }
-                resize(newSize);
+            // intPos <= Integer.MAX_VALUE
+            final int intPos = (int) position;
+            final long newPosition = position + wanted;
+            if (newPosition > IOUtils.SOFT_MAX_ARRAY_LENGTH) {
+                throw new IOException(String.format("Requested array size %,d 
is too large.", newPosition));
             }
-            b.get(data, position, wanted);
-            position += wanted;
-            if (size < position) {
-                size = position;
+            if (newPosition > size) {
+                final int newPositionInt = (int) newPosition;
+                // Ensure that newPositionInt ≤ data.length
+                resize(newPositionInt);
+                size = newPositionInt;
+            }
+            b.get(data, intPos, wanted);
+            position = newPosition;
+            if (size < intPos) {
+                size = intPos;
             }
             return wanted;
         } finally {
diff --git 
a/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java
 
b/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java
index abf96d8f4..afb2245a4 100644
--- 
a/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java
+++ 
b/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java
@@ -47,7 +47,7 @@
  */
 abstract class AbstractSeekableByteChannelTest {
 
-    private SeekableByteChannel channel;
+    protected SeekableByteChannel channel;
 
     @TempDir
     protected Path tempDir;
@@ -87,6 +87,7 @@ void testCloseMultipleTimes() throws IOException {
         assertFalse(channel.isOpen());
     }
 
+
     @Test
     void testConcurrentPositionAndSizeQueries() throws IOException {
         final byte[] data = "test data".getBytes();
@@ -136,6 +137,20 @@ void testPositionBeyondSize() throws IOException {
         assertEquals(4, channel.size()); // Size should not change
     }
 
+    @Test
+    void testPositionBeyondSizeRead() throws IOException {
+        final ByteBuffer buffer = ByteBuffer.allocate(1);
+        channel.position(channel.size() + 1);
+        assertEquals(channel.size() + 1, channel.position());
+        assertEquals(-1, channel.read(buffer));
+        channel.position(Integer.MAX_VALUE + 1L);
+        assertEquals(Integer.MAX_VALUE + 1L, channel.position());
+        assertEquals(-1, channel.read(buffer));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(-1));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(Integer.MIN_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(Long.MIN_VALUE));
+    }
+
     @ParameterizedTest
     @CsvSource({ "0, 0", "5, 5", "10, 10", "100, 100" })
     void testPositionInBounds(final long newPosition, final long 
expectedPosition) throws IOException {
@@ -149,6 +164,7 @@ void testPositionInBounds(final long newPosition, final 
long expectedPosition) t
         assertEquals(expectedPosition, channel.position());
     }
 
+
     @Test
     void testPositionNegative() {
         assertThrows(IllegalArgumentException.class, () -> 
channel.position(-1));
@@ -292,6 +308,18 @@ void testSizeSameOnOverwrite() throws IOException {
         assertEquals(11, channel.size()); // Size should not change
     }
 
+    @Test
+    void testTrucateBeyondSizeReadWrite() throws IOException {
+        final ByteBuffer buffer = ByteBuffer.allocate(1);
+        channel.truncate(channel.size() + 1);
+        assertEquals(-1, channel.read(buffer));
+        channel.truncate(Integer.MAX_VALUE + 1L);
+        assertEquals(-1, channel.read(buffer));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.truncate(-1));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.truncate(Integer.MIN_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.truncate(Long.MIN_VALUE));
+    }
+
     @Test
     void testTruncateNegative() {
         assertThrows(IllegalArgumentException.class, () -> 
channel.truncate(-1));
diff --git 
a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java
 
b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java
index 66c8e60b9..60c294f0d 100644
--- 
a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java
+++ 
b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java
@@ -162,16 +162,39 @@ void testShouldThrowExceptionOnWritingToClosedChannel() {
     }
 
     @Test
-    void testShouldThrowExceptionWhenSettingIncorrectPosition() {
+    void testThrowWhenSettingIncorrectPosition() throws IOException {
         try (ByteArraySeekableByteChannel c = new 
ByteArraySeekableByteChannel()) {
-            assertThrows(IllegalArgumentException.class, () -> 
c.position(Integer.MAX_VALUE + 1L));
+            final ByteBuffer buffer = ByteBuffer.allocate(1);
+            // write
+            c.write(buffer);
+            assertEquals(1, c.position());
+            // bad pos A
+            c.position(c.size() + 1);
+            assertEquals(c.size() + 1, c.position());
+            assertEquals(-1, c.read(buffer));
+            // bad pos B
+            c.position(Integer.MAX_VALUE + 1L);
+            assertEquals(Integer.MAX_VALUE + 1L, c.position());
+            assertEquals(-1, c.read(buffer));
+            assertThrows(IOException.class, () -> c.write(buffer));
+            // negative input is the only illegal input
+            assertThrows(IllegalArgumentException.class, () -> c.position(-1));
+            assertThrows(IllegalArgumentException.class, () -> 
c.position(Integer.MIN_VALUE));
+            assertThrows(IllegalArgumentException.class, () -> 
c.position(Long.MIN_VALUE));
         }
     }
 
     @Test
-    void testShouldThrowExceptionWhenTruncatingToIncorrectSize() {
+    void testThrowWhenTruncatingToIncorrectSize() throws IOException {
         try (ByteArraySeekableByteChannel c = new 
ByteArraySeekableByteChannel()) {
-            assertThrows(IllegalArgumentException.class, () -> 
c.truncate(Integer.MAX_VALUE + 1L));
+            final ByteBuffer buffer = ByteBuffer.allocate(1);
+            c.truncate(c.size() + 1);
+            assertEquals(-1, c.read(buffer));
+            c.truncate(Integer.MAX_VALUE + 1L);
+            assertEquals(-1, c.read(buffer));
+            assertThrows(IllegalArgumentException.class, () -> c.truncate(-1));
+            assertThrows(IllegalArgumentException.class, () -> 
c.truncate(Integer.MIN_VALUE));
+            assertThrows(IllegalArgumentException.class, () -> 
c.truncate(Long.MIN_VALUE));
         }
     }
 
diff --git 
a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java
 
b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java
index 5b29b3f1b..79f971e4a 100644
--- 
a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java
+++ 
b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java
@@ -84,6 +84,22 @@ void testConstructorInvalid() {
         assertThrows(NullPointerException.class, () -> 
ByteArraySeekableByteChannel.wrap(null));
     }
 
+    @Test
+    void testPositionBeyondSizeReadWrite() throws IOException {
+        final ByteBuffer buffer = ByteBuffer.allocate(1);
+        channel.position(channel.size() + 1);
+        assertEquals(channel.size() + 1, channel.position());
+        assertEquals(-1, channel.read(buffer));
+        channel.position(Integer.MAX_VALUE + 1L);
+        assertEquals(Integer.MAX_VALUE + 1L, channel.position());
+        assertEquals(-1, channel.read(buffer));
+        // ByteArraySeekableByteChannel has a hard boundary at 
Integer.MAX_VALUE, files don't.
+        assertThrows(IOException.class, () -> channel.write(buffer));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(-1));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(Integer.MIN_VALUE));
+        assertThrows(IllegalArgumentException.class, () -> 
channel.position(Long.MIN_VALUE));
+    }
+
     @ParameterizedTest
     @MethodSource
     void testShouldResizeWhenWritingMoreDataThanCapacity(final byte[] data, 
final int wanted) throws IOException {

Reply via email to