Skip to content

Commit 6d3ce61

Browse files
authored
Backport Objects.checkFromIndexSize to IOUtils (#790)
* 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. * Add changelog entry * fix: apply `IOUtils.checkIndexFromSize` to all streams * fix: failing tests * fix: inline static import * fix: add usage examples to Javadoc * fix: Javadoc for JDK 8 and 11 * fix: remove `@Override` from code snippet There is no way to insert `@Override` in a Javadoc snippet without triggering a warning in either Java 8 or 21. * fix: `ClosedReader` Javadoc and tests * fix: revert `BrokenReader` and add tests The tests are improved to ensure that the given exception is thrown **before** parameter validation. * fix: remove `BrokenReader.read(char[]/CharBuffer)` overrides * fix: revert `BrokenWriter` Revert `BrokenWriter` and add test cases to ensure the exception is thrown **before** parameter validation. * fix: Checkstyle failures * fix: revert `ClosedWriter` and add tests * fix: revert `FilterCollectionWriter` changes * fix: fix `NullAppendable` Javadoc * fix: fix `NullOutputStream` Javadoc and enhance tests * fix: fix `NullWriter` Javadoc and enhance tests * fix: `NullInputStream` Javadoc * fix: `ClosedOutputStream` Javadoc * fix: `StringBuilderWriter` behavior and Javadoc * fix: restore `NullWriter` comment * fix: `checkFromToIndex` logical or * fix: document `IndexOutOfBoundsException` * fix: `ClosedInputStream` and `ThresholdingOutputStream` javadoc
1 parent 13087b2 commit 6d3ce61

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+726
-161
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ The <action> type attribute can be add,update,fix,remove.
6969
<action dev="ggregory" type="add" due-to="Gary Gregory">Add IOIterable.asIterable().</action>
7070
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add NIO channel support to `AbstractStreamBuilder`.</action>
7171
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Add CloseShieldChannel to close-shielded NIO Channels #786.</action>
72+
<action dev="pkarwasz" type="add" due-to="Piotr P. Karwasz">Added IOUtils.checkFromIndexSize as a Java 8 backport of Objects.checkFromIndexSize #790.</action>
7273
<!-- UPDATE -->
7374
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 85 to 88 #774, #783.</action>
7475
<action type="update" dev="ggregory" due-to="Gary Gregory">[test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0.</action>

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

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

409+
/**
410+
* Validates that the sub-range {@code [off, off + len)} is within the bounds of the given array.
411+
*
412+
* <p>The range is valid if all of the following hold:</p>
413+
* <ul>
414+
* <li>{@code off >= 0}</li>
415+
* <li>{@code len >= 0}</li>
416+
* <li>{@code off + len <= array.length}</li>
417+
* </ul>
418+
*
419+
* <p>If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.</p>
420+
*
421+
* <p>Typical usage in {@link InputStream#read(byte[], int, int)} and {@link OutputStream#write(byte[], int, int)} implementations:</p>
422+
*
423+
* <pre><code>
424+
* public int read(byte[] b, int off, int len) throws IOException {
425+
* IOUtils.checkFromIndexSize(b, off, len);
426+
* if (len == 0) {
427+
* return 0;
428+
* }
429+
* ensureOpen();
430+
* // perform read...
431+
* }
432+
*
433+
* public void write(byte[] b, int off, int len) throws IOException {
434+
* IOUtils.checkFromIndexSize(b, off, len);
435+
* if (len == 0) {
436+
* return;
437+
* }
438+
* ensureOpen();
439+
* // perform write...
440+
* }
441+
* </code></pre>
442+
*
443+
* @param array the array against which the range is validated
444+
* @param off the starting offset into the array (inclusive)
445+
* @param len the number of elements to access
446+
* @throws NullPointerException if {@code array} is {@code null}
447+
* @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code array}
448+
* @see InputStream#read(byte[], int, int)
449+
* @see OutputStream#write(byte[], int, int)
450+
* @since 2.21.0
451+
*/
452+
public static void checkFromIndexSize(final byte[] array, final int off, final int len) {
453+
checkFromIndexSize(off, len, Objects.requireNonNull(array, "byte array").length);
454+
}
455+
456+
/**
457+
* Validates that the sub-range {@code [off, off + len)} is within the bounds of the given array.
458+
*
459+
* <p>The range is valid if all of the following hold:</p>
460+
* <ul>
461+
* <li>{@code off >= 0}</li>
462+
* <li>{@code len >= 0}</li>
463+
* <li>{@code off + len <= array.length}</li>
464+
* </ul>
465+
*
466+
* <p>If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.</p>
467+
*
468+
* <p>Typical usage in {@link Reader#read(char[], int, int)} and {@link Writer#write(char[], int, int)} implementations:</p>
469+
*
470+
* <pre><code>
471+
* public int read(char[] cbuf, int off, int len) throws IOException {
472+
* ensureOpen();
473+
* IOUtils.checkFromIndexSize(cbuf, off, len);
474+
* if (len == 0) {
475+
* return 0;
476+
* }
477+
* // perform read...
478+
* }
479+
*
480+
* public void write(char[] cbuf, int off, int len) throws IOException {
481+
* ensureOpen();
482+
* IOUtils.checkFromIndexSize(cbuf, off, len);
483+
* if (len == 0) {
484+
* return;
485+
* }
486+
* // perform write...
487+
* }
488+
* </code></pre>
489+
*
490+
* @param array the array against which the range is validated
491+
* @param off the starting offset into the array (inclusive)
492+
* @param len the number of characters to access
493+
* @throws NullPointerException if {@code array} is {@code null}
494+
* @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code array}
495+
* @see Reader#read(char[], int, int)
496+
* @see Writer#write(char[], int, int)
497+
* @since 2.21.0
498+
*/
499+
public static void checkFromIndexSize(final char[] array, final int off, final int len) {
500+
checkFromIndexSize(off, len, Objects.requireNonNull(array, "char array").length);
501+
}
502+
503+
/**
504+
* Validates that the sub-range {@code [off, off + len)} is within the bounds of the given string.
505+
*
506+
* <p>The range is valid if all of the following hold:</p>
507+
* <ul>
508+
* <li>{@code off >= 0}</li>
509+
* <li>{@code len >= 0}</li>
510+
* <li>{@code off + len <= str.length()}</li>
511+
* </ul>
512+
*
513+
* <p>If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.</p>
514+
*
515+
* <p>Typical usage in {@link Writer#write(String, int, int)} implementations:</p>
516+
*
517+
* <pre><code>
518+
* public void write(String str, int off, int len) throws IOException {
519+
* IOUtils.checkFromIndexSize(str, off, len);
520+
* if (len == 0) {
521+
* return;
522+
* }
523+
* // perform write...
524+
* }
525+
* </code></pre>
526+
*
527+
* @param str the string against which the range is validated
528+
* @param off the starting offset into the string (inclusive)
529+
* @param len the number of characters to write
530+
* @throws NullPointerException if {@code str} is {@code null}
531+
* @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code str}
532+
* @see Writer#write(String, int, int)
533+
* @since 2.21.0
534+
*/
535+
public static void checkFromIndexSize(final String str, final int off, final int len) {
536+
checkFromIndexSize(off, len, Objects.requireNonNull(str, "str").length());
537+
}
538+
539+
static void checkFromIndexSize(final int off, final int len, final int arrayLength) {
540+
if ((off | len | arrayLength) < 0 || arrayLength - len < off) {
541+
throw new IndexOutOfBoundsException(String.format("Range [%s, %<s + %s) out of bounds for length %s", off, len, arrayLength));
542+
}
543+
}
544+
545+
/**
546+
* Validates that the sub-sequence {@code [fromIndex, toIndex)} is within the bounds of the given {@link CharSequence}.
547+
*
548+
* <p>The sub-sequence is valid if all of the following hold:</p>
549+
* <ul>
550+
* <li>{@code fromIndex >= 0}</li>
551+
* <li>{@code fromIndex <= toIndex}</li>
552+
* <li>{@code toIndex <= seq.length()}</li>
553+
* </ul>
554+
*
555+
* <p>If {@code seq} is {@code null}, it is treated as the literal string {@code "null"} (length {@code 4}).</p>
556+
*
557+
* <p>If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.</p>
558+
*
559+
* <p>Typical usage in {@link Appendable#append(CharSequence, int, int)} implementations:</p>
560+
*
561+
* <pre><code>
562+
* public Appendable append(CharSequence csq, int start, int end) throws IOException {
563+
* IOUtils.checkFromToIndex(csq, start, end);
564+
* // perform append...
565+
* return this;
566+
* }
567+
* </code></pre>
568+
*
569+
* @param seq the character sequence to validate (may be {@code null}, treated as {@code "null"})
570+
* @param fromIndex the starting index (inclusive)
571+
* @param toIndex the ending index (exclusive)
572+
* @throws IndexOutOfBoundsException if the range {@code [fromIndex, toIndex)} is out of bounds for {@code seq}
573+
* @see Appendable#append(CharSequence, int, int)
574+
* @since 2.21.0
575+
*/
576+
public static void checkFromToIndex(final CharSequence seq, final int fromIndex, final int toIndex) {
577+
checkFromToIndex(fromIndex, toIndex, seq != null ? seq.length() : 4);
578+
}
579+
580+
static void checkFromToIndex(final int fromIndex, final int toIndex, final int length) {
581+
if (fromIndex < 0 || toIndex < fromIndex || length < toIndex) {
582+
throw new IndexOutOfBoundsException(String.format("Range [%s, %s) out of bounds for length %s", fromIndex, toIndex, length));
583+
}
584+
}
585+
409586
/**
410587
* Clears any state.
411588
* <ul>

src/main/java/org/apache/commons/io/input/BOMInputStream.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,8 +419,10 @@ public int read() throws IOException {
419419
* Invokes the delegate's {@code read(byte[])} method, detecting and optionally skipping BOM.
420420
*
421421
* @param buf
422-
* the buffer to read the bytes into
422+
* the buffer to read the bytes into, never {@code null}
423423
* @return the number of bytes read (excluding BOM) or -1 if the end of stream
424+
* @throws NullPointerException
425+
* if the buffer is {@code null}
424426
* @throws IOException
425427
* if an I/O error occurs
426428
*/
@@ -439,11 +441,19 @@ public int read(final byte[] buf) throws IOException {
439441
* @param len
440442
* The number of bytes to read (excluding BOM)
441443
* @return the number of bytes read or -1 if the end of stream
444+
* @throws NullPointerException
445+
* if the buffer is {@code null}
446+
* @throws IndexOutOfBoundsException
447+
* if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code buf.length}
442448
* @throws IOException
443449
* if an I/O error occurs
444450
*/
445451
@Override
446452
public int read(final byte[] buf, int off, int len) throws IOException {
453+
IOUtils.checkFromIndexSize(buf, off, len);
454+
if (len == 0) {
455+
return 0;
456+
}
447457
int firstCount = 0;
448458
int b = 0;
449459
while (len > 0 && b >= 0) {

src/main/java/org/apache/commons/io/input/BoundedReader.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.io.IOException;
2424
import java.io.Reader;
2525

26+
import org.apache.commons.io.IOUtils;
27+
2628
/**
2729
* A reader that imposes a limit to the number of characters that can be read from an underlying reader, returning EOF
2830
* when this limit is reached, regardless of state of underlying reader.
@@ -116,11 +118,14 @@ public int read() throws IOException {
116118
* @param off The offset
117119
* @param len The number of chars to read
118120
* @return the number of chars read
121+
* @throws NullPointerException if the buffer is {@code null}.
122+
* @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code cbuf.length}.
119123
* @throws IOException If an I/O error occurs while calling the underlying reader's read method
120124
* @see Reader#read(char[], int, int)
121125
*/
122126
@Override
123127
public int read(final char[] cbuf, final int off, final int len) throws IOException {
128+
IOUtils.checkFromIndexSize(cbuf, off, len);
124129
int c;
125130
for (int i = 0; i < len; i++) {
126131
c = read();

src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,9 @@ public synchronized int read() throws IOException {
254254

255255
@Override
256256
public synchronized int read(final byte[] b, final int offset, int len) throws IOException {
257-
if (offset < 0 || len < 0 || offset + len < 0 || offset + len > b.length) {
258-
throw new IndexOutOfBoundsException();
257+
IOUtils.checkFromIndexSize(b, offset, len);
258+
if (len == 0) {
259+
return 0;
259260
}
260261
if (!refill()) {
261262
return EOF;

src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.nio.charset.CharsetEncoder;
2929
import java.nio.charset.CoderResult;
3030
import java.nio.charset.CodingErrorAction;
31-
import java.util.Objects;
3231

3332
import org.apache.commons.io.Charsets;
3433
import org.apache.commons.io.IOUtils;
@@ -319,10 +318,7 @@ public int read(final byte[] b) throws IOException {
319318

320319
@Override
321320
public int read(final byte[] array, int off, int len) throws IOException {
322-
Objects.requireNonNull(array, "array");
323-
if (len < 0 || off + len > array.length) {
324-
throw new IndexOutOfBoundsException("Array Size=" + array.length + ", offset=" + off + ", length=" + len);
325-
}
321+
IOUtils.checkFromIndexSize(array, off, len);
326322
if (len == 0) {
327323
return 0; // must return 0 for zero length read
328324
}

src/main/java/org/apache/commons/io/input/CharSequenceReader.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
import java.io.Reader;
2222
import java.io.Serializable;
23-
import java.util.Objects;
23+
24+
import org.apache.commons.io.IOUtils;
2425

2526
/**
2627
* {@link Reader} implementation that can read from String, StringBuffer,
@@ -204,19 +205,19 @@ public int read() {
204205
* @param array The array to store the characters in
205206
* @param offset The starting position in the array to store
206207
* @param length The maximum number of characters to read
207-
* @return The number of characters read or -1 if there are
208-
* no more
208+
* @return The number of characters read or -1 if there are no more
209+
* @throws NullPointerException if the array is {@code null}.
210+
* @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code array.length}.
209211
*/
210212
@Override
211213
public int read(final char[] array, final int offset, final int length) {
214+
IOUtils.checkFromIndexSize(array, offset, length);
215+
if (length == 0) {
216+
return 0;
217+
}
212218
if (idx >= end()) {
213219
return EOF;
214220
}
215-
Objects.requireNonNull(array, "array");
216-
if (length < 0 || offset < 0 || offset + length > array.length) {
217-
throw new IndexOutOfBoundsException("Array Size=" + array.length +
218-
", offset=" + offset + ", length=" + length);
219-
}
220221

221222
if (charSequence instanceof String) {
222223
final int count = Math.min(length, end() - idx);

src/main/java/org/apache/commons/io/input/ClosedInputStream.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,19 @@ public int read() {
7979
/**
8080
* Returns {@code -1} to indicate that the stream is closed.
8181
*
82-
* @param b ignored.
83-
* @param off ignored.
84-
* @param len ignored.
85-
* @return always -1.
82+
* @param b The buffer to read bytes into.
83+
* @param off The start offset.
84+
* @param len The number of bytes to read.
85+
* @return If len is zero, then {@code 0}; otherwise {@code -1}.
86+
* @throws NullPointerException if the byte array is {@code null}.
87+
* @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}.
8688
*/
8789
@Override
8890
public int read(final byte[] b, final int off, final int len) throws IOException {
91+
IOUtils.checkFromIndexSize(b, off, len);
92+
if (len == 0) {
93+
return 0;
94+
}
8995
return EOF;
9096
}
9197

src/main/java/org/apache/commons/io/input/ClosedReader.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,26 @@ public void close() throws IOException {
6262
}
6363

6464
/**
65-
* Returns -1 to indicate that the stream is closed.
65+
* A no-op read method that always indicates end-of-stream.
6666
*
67-
* @param cbuf ignored
68-
* @param off ignored
69-
* @param len ignored
70-
* @return always -1
67+
* <p>Behavior:</p>
68+
* <ul>
69+
* <li>If {@code len == 0}, returns {@code 0} immediately (no characters are read).</li>
70+
* <li>Otherwise, always returns {@value IOUtils#EOF} to signal that the stream is closed or at end-of-stream.</li>
71+
* </ul>
72+
*
73+
* @param cbuf The destination buffer.
74+
* @param off The offset at which to start storing characters.
75+
* @param len The maximum number of characters to read.
76+
* @return {@code 0} if {@code len == 0}; otherwise always {@value IOUtils#EOF}.
77+
* @throws IndexOutOfBoundsException If {@code off < 0}, {@code len < 0}, or {@code off + len > cbuf.length}.
7178
*/
7279
@Override
7380
public int read(final char[] cbuf, final int off, final int len) {
81+
IOUtils.checkFromIndexSize(cbuf, off, len);
82+
if (len == 0) {
83+
return 0;
84+
}
7485
return EOF;
7586
}
7687

src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.nio.file.Path;
2828
import java.nio.file.StandardOpenOption;
2929

30+
import org.apache.commons.io.IOUtils;
3031
import org.apache.commons.io.build.AbstractStreamBuilder;
3132

3233
/**
@@ -216,6 +217,10 @@ public int read() throws IOException {
216217

217218
@Override
218219
public int read(final byte[] b, final int off, final int len) throws IOException {
220+
IOUtils.checkFromIndexSize(b, off, len);
221+
if (len == 0) {
222+
return 0;
223+
}
219224
checkOpen();
220225
if (!buffer.hasRemaining()) {
221226
nextBuffer();

0 commit comments

Comments
 (0)