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-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 5d46a39e4 RandomStringUtils.random() methods may not return when asked
to generate only letters or only numbers while provided with a range that
contains neither
5d46a39e4 is described below
commit 5d46a39e450f5dd69027329c9bc0542c113a04be
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Jan 19 09:44:35 2026 -0500
RandomStringUtils.random() methods may not return when asked to generate
only letters or only numbers while provided with a range that contains
neither
---
src/changes/changes.xml | 1 +
.../apache/commons/lang3/RandomStringUtils.java | 59 ++++++++++++----------
.../commons/lang3/RandomStringUtilsTest.java | 14 +++++
3 files changed, 46 insertions(+), 28 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 5aac073ac..c098e1602 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -102,6 +102,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="ThrawnCA, Gary
Gregory">Fix handling of null marker in StringUtils.abbreviate(String, String,
int, int) #1571.</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Better exception messages from FastDateParser.parse(String).</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory,
David Du">Simplify NumberUtils.isParsable(String) #1570.</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">RandomStringUtils.random() methods may not return when asked to
generate only letters or only numbers while provided with a range that contains
neither.</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
b/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
index aae4f3446..1e42a7d15 100644
--- a/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
@@ -243,7 +243,7 @@ public static String random(final int count, final int
start, final int end, fin
* @param start the position in set of chars to start at (inclusive).
* @param end the position in set of chars to end before (exclusive).
* @param letters if {@code true}, generated string may include alphabetic
characters.
- * @param numbers if {@code true}, generated string may include numeric
characters.
+ * @param digits if {@code true}, generated string may include digit
characters.
* @param chars the set of chars to choose randoms from, must not be
empty. If {@code null}, then it will use the
* set of all chars.
* @param random a source of randomness.
@@ -252,7 +252,7 @@ public static String random(final int count, final int
start, final int end, fin
* @throws IllegalArgumentException if {@code count} < 0 or the
provided chars array is empty.
* @since 2.0
*/
- public static String random(int count, int start, int end, final boolean
letters, final boolean numbers,
+ public static String random(int count, int start, int end, final boolean
letters, final boolean digits,
final char[] chars, final Random random) {
if (count == 0) {
return StringUtils.EMPTY;
@@ -263,29 +263,25 @@ public static String random(int count, int start, int
end, final boolean letters
if (chars != null && chars.length == 0) {
throw new IllegalArgumentException("The chars array must not be
empty");
}
-
if (start == 0 && end == 0) {
if (chars != null) {
end = chars.length;
- } else if (!letters && !numbers) {
+ } else if (!letters && !digits) {
end = Character.MAX_CODE_POINT;
} else {
end = 'z' + 1;
start = ' ';
}
} else if (end <= start) {
- throw new IllegalArgumentException(
- "Parameter end (" + end + ") must be greater than start ("
+ start + ")");
+ throw new IllegalArgumentException("Parameter end (" + end + ")
must be greater than start (" + start + ")");
} else if (start < 0 || end < 0) {
throw new IllegalArgumentException("Character positions MUST be >=
0");
}
-
if (end > Character.MAX_CODE_POINT) {
// Technically, it should be `Character.MAX_CODE_POINT+1` as `end`
is excluded
// But the character `Character.MAX_CODE_POINT` is private use, so
it would anyway be excluded
end = Character.MAX_CODE_POINT;
}
-
// Optimizations and tests when chars == null and using ASCII
characters (end <= 0x7f)
if (chars == null && end <= 0x7f) {
// Optimize generation of full alphanumerical characters
@@ -293,16 +289,13 @@ public static String random(int count, int start, int
end, final boolean letters
// In turn, this would make us reject the sampling with
probability 1 - 62 / 2^7 > 1 / 2
// Instead we can pick directly from the right set of 62
characters, which requires
// picking a 6-bit integer and only rejecting with probability 2 /
64 = 1 / 32
- if (letters && numbers && start <= ASCII_0 && end >= ASCII_z + 1) {
+ if (letters && digits && start <= ASCII_0 && end >= ASCII_z + 1) {
return random(count, 0, 0, false, false, ALPHANUMERICAL_CHARS,
random);
}
-
- if (numbers && end <= ASCII_0 || letters && end <= ASCII_A) {
- throw new IllegalArgumentException(
- "Parameter end (" + end + ") must be greater than (" +
ASCII_0 + ") for generating digits "
- + "or greater than (" + ASCII_A + ") for
generating letters.");
+ if (digits && end <= ASCII_0 || letters && end <= ASCII_A) {
+ throw new IllegalArgumentException("Parameter end (" + end +
") must be greater than (" + ASCII_0 + ") for generating digits "
+ + "or greater than (" + ASCII_A + ") for generating
letters.");
}
-
// Optimize start and end when filtering by letters and/or numbers:
// The range provided may be too large since we filter anyway
afterward.
// Note the use of Math.min/max (as opposed to setting start to
'0' for example),
@@ -311,10 +304,10 @@ public static String random(int count, int start, int
end, final boolean letters
// needs to stay equal to '1' in that case.
// Note that because of the above test, we will always have start
< end
// even after this optimization.
- if (letters && numbers) {
+ if (letters && digits) {
start = Math.max(ASCII_0, start);
end = Math.min(ASCII_z + 1, end);
- } else if (numbers) {
+ } else if (digits) {
// just numbers, no letters
start = Math.max(ASCII_0, start);
end = Math.min(ASCII_9 + 1, end);
@@ -324,7 +317,26 @@ public static String random(int count, int start, int end,
final boolean letters
end = Math.min(ASCII_z + 1, end);
}
}
-
+ if (letters && !digits) {
+ for (int i = start; i < end; i++) {
+ if (Character.isLetter(i)) {
+ break;
+ }
+ if (i == end - 1) {
+ throw new IllegalArgumentException(String.format("No
letters exist between start %,d and end %,d.", start, end));
+ }
+ }
+ }
+ if (!letters && digits) {
+ for (int i = start; i < end; i++) {
+ if (Character.isDigit(i)) {
+ break;
+ }
+ if (i == end - 1) {
+ throw new IllegalArgumentException(String.format("No
digits exist between start %,d and end %,d.", start, end));
+ }
+ }
+ }
final StringBuilder builder = new StringBuilder(count);
final int gap = end - start;
final int gapBits = Integer.SIZE - Integer.numberOfLeadingZeros(gap);
@@ -343,7 +355,6 @@ public static String random(int count, int start, int end,
final boolean letters
final long desiredCacheSize = ((long) count * gapBits +
CACHE_PADDING_BITS) / BITS_TO_BYTES_DIVISOR + BASE_CACHE_SIZE_PADDING;
final int cacheSize = (int) Math.min(desiredCacheSize,
Integer.MAX_VALUE / BITS_TO_BYTES_DIVISOR + BASE_CACHE_SIZE_PADDING);
final CachedRandomBits arb = new CachedRandomBits(cacheSize, random);
-
while (count-- != 0) {
// Generate a random value between start (included) and end
(excluded)
final int randomValue = arb.nextBits(gapBits) + start;
@@ -352,11 +363,9 @@ public static String random(int count, int start, int end,
final boolean letters
count++;
continue;
}
-
final int codePoint;
if (chars == null) {
codePoint = randomValue;
-
switch (Character.getType(codePoint)) {
case Character.UNASSIGNED:
case Character.PRIVATE_USE:
@@ -364,25 +373,19 @@ public static String random(int count, int start, int
end, final boolean letters
count++;
continue;
}
-
} else {
codePoint = chars[randomValue];
}
-
final int numberOfChars = Character.charCount(codePoint);
if (count == 0 && numberOfChars > 1) {
count++;
continue;
}
-
- if (letters && Character.isLetter(codePoint) || numbers &&
Character.isDigit(codePoint)
- || !letters && !numbers) {
+ if (letters && Character.isLetter(codePoint) || digits &&
Character.isDigit(codePoint) || !letters && !digits) {
builder.appendCodePoint(codePoint);
-
if (numberOfChars == 2) {
count--;
}
-
} else {
count++;
}
diff --git a/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
b/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
index e6759c465..e5622c94e 100644
--- a/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
@@ -20,6 +20,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -29,6 +30,7 @@
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -178,6 +180,18 @@ void testExceptionsRandomPrint(final RandomStringUtils
rsu) {
assertIllegalArgumentException(() -> rsu.nextPrint(-1));
}
+ @Test
+ @Timeout(value = 2, threadMode = Timeout.ThreadMode.SAME_THREAD)
+ void testFilterLetters() {
+ assertThrows(IllegalArgumentException.class, () ->
RandomStringUtils.random(5, 0x80, 0xA0, true, false, null, new Random()));
+ }
+
+ @Test
+ @Timeout(value = 2, threadMode = Timeout.ThreadMode.SAME_THREAD)
+ void testFilterNumbers() {
+ assertThrows(IllegalArgumentException.class, () ->
RandomStringUtils.random(5, 0x80, 0xA0, false, true, null, new Random()));
+ }
+
/**
* Test homogeneity of random strings generated -- i.e., test that
characters show up with expected frequencies in generated strings. Will fail
randomly
* about 1 in 100,000 times. Repeated failures indicate a problem.