Skip to content

Commit bb3226a

Browse files
committed
Backport Objects.checkFromIndexSize to IOUtils
Java 9 introduced `Objects.checkFromIndexSize` for validating the offset/length ranges used in `read` and `write` operations. This PR adds an equivalent method, `IOUtils.checkFromIndexSize`, to provide the same validation in Commons IO. It simplifies range checks in stream implementations and makes the utility available to projects that still target Java 8.
1 parent ec73c85 commit bb3226a

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

src/main/java/org/apache/commons/io/IOUtils.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,36 @@ private static char[] charArray(final int size) {
406406
return new char[size];
407407
}
408408

409+
/**
410+
* Checks if the sub-range described by an offset and length is valid for an array of the given length.
411+
*
412+
* <p>This method is functionally equivalent to
413+
* {@code java.util.Objects#checkFromIndexSize(int, int, int)} introduced in Java 9,
414+
* but is provided here for use on Java 8.</p>
415+
*
416+
* <p>The range is valid if all of the following hold:</p>
417+
* <ul>
418+
* <li>{@code off >= 0}</li>
419+
* <li>{@code len >= 0}</li>
420+
* <li>{@code arrayLength >= 0}</li>
421+
* <li>{@code off + len <= arrayLength}</li>
422+
* </ul>
423+
*
424+
* <p>If the range is invalid, this method throws an
425+
* {@link IndexOutOfBoundsException} with a descriptive message.</p>
426+
*
427+
* @param off the starting offset into the array
428+
* @param len the number of elements in the range
429+
* @param arrayLength the length of the array to be checked against
430+
* @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds
431+
* @since 2.21.0
432+
*/
433+
public static void checkFromIndexSize(final int off, final int len, final int arrayLength) {
434+
if ((off | len | arrayLength) < 0 || arrayLength - len < off) {
435+
throw new IndexOutOfBoundsException(String.format("Range [%s, %<s + %s) out of bounds for length %s", off, len, arrayLength));
436+
}
437+
}
438+
409439
/**
410440
* Clears any state.
411441
* <ul>

src/test/java/org/apache/commons/io/IOUtilsTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.io.SequenceInputStream;
5151
import java.io.StringReader;
5252
import java.io.Writer;
53+
import java.lang.reflect.InvocationTargetException;
5354
import java.net.ServerSocket;
5455
import java.net.Socket;
5556
import java.net.URI;
@@ -65,6 +66,7 @@
6566
import java.util.Arrays;
6667
import java.util.Collections;
6768
import java.util.List;
69+
import java.util.Objects;
6870
import java.util.concurrent.atomic.AtomicBoolean;
6971
import java.util.function.Consumer;
7072
import java.util.function.Supplier;
@@ -85,7 +87,9 @@
8587
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
8688
import org.apache.commons.io.test.TestUtils;
8789
import org.apache.commons.io.test.ThrowOnCloseReader;
90+
import org.apache.commons.lang3.JavaVersion;
8891
import org.apache.commons.lang3.StringUtils;
92+
import org.apache.commons.lang3.SystemUtils;
8993
import org.apache.commons.lang3.exception.ExceptionUtils;
9094
import org.junit.jupiter.api.AfterAll;
9195
import org.junit.jupiter.api.BeforeAll;
@@ -348,6 +352,54 @@ void testByteArrayWithNegativeSize() {
348352
assertThrows(NegativeArraySizeException.class, () -> IOUtils.byteArray(-1));
349353
}
350354

355+
static Stream<Arguments> testCheckFromIndexSizeValidCases() {
356+
return Stream.of(
357+
// Valid cases
358+
Arguments.of(0, 0, 42),
359+
Arguments.of(0, 1, 42),
360+
Arguments.of(0, 42, 42),
361+
Arguments.of(41, 1, 42),
362+
Arguments.of(42, 0, 42)
363+
);
364+
}
365+
366+
@ParameterizedTest
367+
@MethodSource
368+
void testCheckFromIndexSizeValidCases(int off, int len, int arrayLength) {
369+
assertDoesNotThrow(() -> IOUtils.checkFromIndexSize(off, len, arrayLength));
370+
}
371+
372+
static Stream<Arguments> testCheckFromIndexSizeInvalidCases() {
373+
return Stream.of(
374+
Arguments.of(-1, 0, 42),
375+
Arguments.of(0, -1, 42),
376+
Arguments.of(0, 0, -1),
377+
// off + len > arrayLength
378+
Arguments.of(1, 42, 42),
379+
Arguments.of(Integer.MAX_VALUE, 1, Integer.MAX_VALUE)
380+
);
381+
}
382+
383+
@ParameterizedTest
384+
@MethodSource
385+
void testCheckFromIndexSizeInvalidCases(int off, int len, int arrayLength) {
386+
final IndexOutOfBoundsException ex = assertThrows(IndexOutOfBoundsException.class, () -> IOUtils.checkFromIndexSize(off, len, arrayLength));
387+
assertTrue(ex.getMessage().contains(String.valueOf(off)));
388+
assertTrue(ex.getMessage().contains(String.valueOf(len)));
389+
assertTrue(ex.getMessage().contains(String.valueOf(arrayLength)));
390+
// Optional requirement: compare the exception message for Java 8 and Java 9+
391+
if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) {
392+
final IndexOutOfBoundsException jreEx = assertThrows(IndexOutOfBoundsException.class, () -> {
393+
try {
394+
Objects.class.getDeclaredMethod("checkFromIndexSize", int.class, int.class, int.class).invoke(null, off, len, arrayLength);
395+
} catch (InvocationTargetException ite) {
396+
throw ite.getTargetException();
397+
}
398+
});
399+
assertEquals(jreEx.getMessage(), ex.getMessage());
400+
}
401+
}
402+
351403
@Test
352404
void testClose() {
353405
assertDoesNotThrow(() -> IOUtils.close((Closeable) null));

0 commit comments

Comments
 (0)