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} &lt; 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.

Reply via email to