This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-rng.git
The following commit(s) were added to refs/heads/master by this push: new c3c8021 RNG-170: Ensure nextBytes is consistent with JDK range checks c3c8021 is described below commit c3c802117da90523ecc0dd1507d895ac9c3f5f54 Author: Alex Herbert <aherb...@apache.org> AuthorDate: Wed Mar 16 00:06:47 2022 +0000 RNG-170: Ensure nextBytes is consistent with JDK range checks Updated to match behaviour of System.arraycopy and JDK 9 Objects.checkFromIndexSize. This now allows: nextBytes(new byte[0], 0, 0) nextBytes(new byte[10], 10, 0) Previously these would throw an exception. --- .../commons/rng/core/source32/IntProvider.java | 41 ++++++++++++++++++-- .../commons/rng/core/source64/LongProvider.java | 41 ++++++++++++++++++-- .../apache/commons/rng/core/BaseProviderTest.java | 45 ++++++++++++++++++++++ .../rng/core/ProvidersCommonParametricTest.java | 19 ++++++++- .../commons/rng/core/source32/IntProviderTest.java | 35 +++++++++++++++++ .../rng/core/source64/LongProviderTest.java | 35 +++++++++++++++++ src/changes/changes.xml | 5 +++ .../resources/spotbugs/spotbugs-exclude-filter.xml | 10 +++++ 8 files changed, 224 insertions(+), 7 deletions(-) diff --git a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/IntProvider.java b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/IntProvider.java index 9a4ab7a..57d91e0 100644 --- a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/IntProvider.java +++ b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/IntProvider.java @@ -152,9 +152,7 @@ public abstract class IntProvider public void nextBytes(byte[] bytes, int start, int len) { - checkIndex(0, bytes.length - 1, start); - checkIndex(0, bytes.length - start, len); - + checkFromIndexSize(start, len, bytes.length); nextBytesFill(this, bytes, start, len); } @@ -206,4 +204,41 @@ public abstract class IntProvider } } } + + /** + * Checks if the sub-range from fromIndex (inclusive) to fromIndex + size (exclusive) is + * within the bounds of range from 0 (inclusive) to length (exclusive). + * + * <p>This function provides the functionality of + * {@code java.utils.Objects.checkFromIndexSize} introduced in JDK 9. The + * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Objects.html#checkFromIndexSize(int,int,int)">Objects</a> + * javadoc has been reproduced for reference. + * + * <p>The sub-range is defined to be out of bounds if any of the following inequalities + * is true: + * <ul> + * <li>{@code fromIndex < 0} + * <li>{@code size < 0} + * <li>{@code fromIndex + size > length}, taking into account integer overflow + * <li>{@code length < 0}, which is implied from the former inequalities + * </ul> + * + * @param fromIndex the lower-bound (inclusive) of the sub-interval + * @param size the size of the sub-range + * @param length the upper-bound (exclusive) of the range + * @return the fromIndex + * @throws IndexOutOfBoundsException if the sub-range is out of bounds + */ + private static int checkFromIndexSize(int fromIndex, int size, int length) { + // check for any negatives, + // or overflow safe length check given the values are all positive + // remaining = length - fromIndex + if ((fromIndex | size | length) < 0 || size > length - fromIndex) { + throw new IndexOutOfBoundsException( + // Note: %<d is 'relative indexing' to re-use the last argument + String.format("Range [%d, %<d + %d) out of bounds for length %d", + fromIndex, size, length)); + } + return fromIndex; + } } diff --git a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LongProvider.java b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LongProvider.java index 278c5bb..7660d32 100644 --- a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LongProvider.java +++ b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LongProvider.java @@ -184,9 +184,7 @@ public abstract class LongProvider public void nextBytes(byte[] bytes, int start, int len) { - checkIndex(0, bytes.length - 1, start); - checkIndex(0, bytes.length - start, len); - + checkFromIndexSize(start, len, bytes.length); nextBytesFill(this, bytes, start, len); } @@ -242,4 +240,41 @@ public abstract class LongProvider } } } + + /** + * Checks if the sub-range from fromIndex (inclusive) to fromIndex + size (exclusive) is + * within the bounds of range from 0 (inclusive) to length (exclusive). + * + * <p>This function provides the functionality of + * {@code java.utils.Objects.checkFromIndexSize} introduced in JDK 9. The + * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Objects.html#checkFromIndexSize(int,int,int)">Objects</a> + * javadoc has been reproduced for reference. + * + * <p>The sub-range is defined to be out of bounds if any of the following inequalities + * is true: + * <ul> + * <li>{@code fromIndex < 0} + * <li>{@code size < 0} + * <li>{@code fromIndex + size > length}, taking into account integer overflow + * <li>{@code length < 0}, which is implied from the former inequalities + * </ul> + * + * @param fromIndex the lower-bound (inclusive) of the sub-interval + * @param size the size of the sub-range + * @param length the upper-bound (exclusive) of the range + * @return the fromIndex + * @throws IndexOutOfBoundsException if the sub-range is out of bounds + */ + private static int checkFromIndexSize(int fromIndex, int size, int length) { + // check for any negatives, + // or overflow safe length check given the values are all positive + // remaining = length - fromIndex + if ((fromIndex | size | length) < 0 || size > length - fromIndex) { + throw new IndexOutOfBoundsException( + // Note: %<d is 'relative indexing' to re-use the last argument + String.format("Range [%d, %<d + %d) out of bounds for length %d", + fromIndex, size, length)); + } + return fromIndex; + } } diff --git a/commons-rng-core/src/test/java/org/apache/commons/rng/core/BaseProviderTest.java b/commons-rng-core/src/test/java/org/apache/commons/rng/core/BaseProviderTest.java index 207ed7f..1eeaac6 100644 --- a/commons-rng-core/src/test/java/org/apache/commons/rng/core/BaseProviderTest.java +++ b/commons-rng-core/src/test/java/org/apache/commons/rng/core/BaseProviderTest.java @@ -83,6 +83,51 @@ class BaseProviderTest { } /** + * Test the checkIndex method. This is not used by any RNG implementations + * as it has been superseded by the equivalent of JDK 9 Objects.checkFromIndexSize. + */ + @Test + void testCheckIndex() { + final BaseProvider rng = new BaseProvider() { + @Override + public void nextBytes(byte[] bytes) { /* noop */ } + @Override + public void nextBytes(byte[] bytes, int start, int len) { /* noop */ } + @Override + public int nextInt() { + return 0; + } + @Override + public long nextLong() { + return 0; + } + @Override + public boolean nextBoolean() { + return false; + } + @Override + public float nextFloat() { + return 0; + } + @Override + public double nextDouble() { + return 0; + } + }; + // Arguments are (min, max, index) + // index must be in [min, max] + rng.checkIndex(-10, 5, 0); + rng.checkIndex(-10, 5, -10); + rng.checkIndex(-10, 5, 5); + rng.checkIndex(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE); + rng.checkIndex(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, -11)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, 6)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, Integer.MIN_VALUE)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> rng.checkIndex(-10, 5, Integer.MAX_VALUE)); + } + + /** * Dummy class for checking the behaviorof the IntProvider. Tests: * <ul> * <li>an incomplete implementation</li> diff --git a/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersCommonParametricTest.java b/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersCommonParametricTest.java index f390d10..6935b90 100644 --- a/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersCommonParametricTest.java +++ b/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersCommonParametricTest.java @@ -56,14 +56,31 @@ class ProvidersCommonParametricTest { @ParameterizedTest @MethodSource("getList") void testPreconditionNextBytes(UniformRandomProvider generator) { + Assertions.assertThrows(NullPointerException.class, () -> generator.nextBytes(null, 0, 0)); final int size = 10; final int num = 1; final byte[] buf = new byte[size]; Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, -1, num)); - Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, size, 0)); + // Edge-case allowed by JDK range checks (see RNG-170) + generator.nextBytes(buf, size, 0); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, size, 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, size, -1)); final int offset = 2; Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, offset, size - offset + 1)); Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, offset, -1)); + // offset + length overflows + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(buf, offset, Integer.MAX_VALUE)); + // Should be OK + generator.nextBytes(buf, 0, size); + generator.nextBytes(buf, 0, size - offset); + generator.nextBytes(buf, offset, size - offset); + // Should be consistent with no length + final byte[] empty = {}; + generator.nextBytes(empty); + generator.nextBytes(empty, 0, 0); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(empty, 0, 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(empty, 0, -1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> generator.nextBytes(empty, -1, 0)); } // Uniformity tests diff --git a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/IntProviderTest.java b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/IntProviderTest.java index 4947a63..ef4a9fe 100644 --- a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/IntProviderTest.java +++ b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/IntProviderTest.java @@ -18,6 +18,8 @@ package org.apache.commons.rng.core.source32; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; /** * The tests the caching of calls to {@link IntProvider#nextInt()} are used as @@ -75,4 +77,37 @@ class IntProviderTest { } } } + + @ParameterizedTest + @CsvSource({ + // OK + "10, 0, 10", + "10, 5, 5", + "10, 9, 1", + // Allowed edge cases + "0, 0, 0", + "10, 10, 0", + // Bad + "10, 0, 11", + "10, 10, 1", + "10, 10, 2147483647", + "10, 0, -1", + "10, 5, -1", + }) + void testNextBytesIndices(int size, int start, int len) { + final FlipIntProvider rng = new FlipIntProvider(999); + final byte[] bytes = new byte[size]; + // Be consistent with System.arraycopy + try { + System.arraycopy(bytes, start, bytes, start, len); + } catch (IndexOutOfBoundsException ex) { + // nextBytes should throw under the same conditions. + // Note: This is not ArrayIndexOutOfBoundException to be + // future compatible with Objects.checkFromIndexSize. + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> + rng.nextBytes(bytes, start, len)); + return; + } + rng.nextBytes(bytes, start, len); + } } diff --git a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LongProviderTest.java b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LongProviderTest.java index b66ce87..f799999 100644 --- a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LongProviderTest.java +++ b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LongProviderTest.java @@ -18,6 +18,8 @@ package org.apache.commons.rng.core.source64; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; /** * The tests the caching of calls to {@link LongProvider#nextLong()} are used as @@ -116,4 +118,37 @@ class LongProviderTest { } } } + + @ParameterizedTest + @CsvSource({ + // OK + "10, 0, 10", + "10, 5, 5", + "10, 9, 1", + // Allowed edge cases + "0, 0, 0", + "10, 10, 0", + // Bad + "10, 0, 11", + "10, 10, 1", + "10, 10, 2147483647", + "10, 0, -1", + "10, 5, -1", + }) + void testNextBytesIndices(int size, int start, int len) { + final FixedLongProvider rng = new FixedLongProvider(999); + final byte[] bytes = new byte[size]; + // Be consistent with System.arraycopy + try { + System.arraycopy(bytes, start, bytes, start, len); + } catch (IndexOutOfBoundsException ex) { + // nextBytes should throw under the same conditions. + // Note: This is not ArrayIndexOutOfBoundException to be + // future compatible with Objects.checkFromIndexSize. + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> + rng.nextBytes(bytes, start, len)); + return; + } + rng.nextBytes(bytes, start, len); + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index c37cf53..ee36c92 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -81,6 +81,11 @@ re-run tests that fail, and pass the build if they succeed within the allotted number of reruns (the test will be marked as 'flaky' in the report). "> + <action dev="aherbert" type="fix" issue="170"> + Update implementations of "UniformRandomProvider.nextBytes" with a range + [start, start + length) to be consistent with the exception conditions of the + JDK array range checks. + </action> <action dev="aherbert" type="add" issue="167"> New "TSampler" class to sample from Student's t-distribution. </action> diff --git a/src/main/resources/spotbugs/spotbugs-exclude-filter.xml b/src/main/resources/spotbugs/spotbugs-exclude-filter.xml index a4a9e08..f50d6a4 100644 --- a/src/main/resources/spotbugs/spotbugs-exclude-filter.xml +++ b/src/main/resources/spotbugs/spotbugs-exclude-filter.xml @@ -107,4 +107,14 @@ <BugPattern name="FE_FLOATING_POINT_EQUALITY"/> </Match> + <!-- False positives for range checks. The return value matches the argument. --> + <Match> + <Or> + <Class name="org.apache.commons.rng.core.source32.IntProvider"/> + <Class name="org.apache.commons.rng.core.source64.LongProvider"/> + </Or> + <Method name="nextBytes"/> + <BugPattern name="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/> + </Match> + </FindBugsFilter>