This is an automated email from the ASF dual-hosted git repository.

garydgregory 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 86ed42da5 Fix RandomStringUtils.random() false rejection of letters 
and digits (#1703)
86ed42da5 is described below

commit 86ed42da5c1ac09888dfe2481aaa06df13fb1a68
Author: alhuda <[email protected]>
AuthorDate: Mon Jun 15 17:27:39 2026 +0530

    Fix RandomStringUtils.random() false rejection of letters and digits (#1703)
    
    * fix RandomStringUtils.random false rejection of letters and digits
    
    * reject empty letters && digits ASCII range with a clear error
    
    When letters && digits clamps the range above the alphanumerics
    (e.g. ['z'+1, 0x7f)) it collapses to start >= end, which made gap 0
    and nextBits(0) throw the unrelated "number of bits must be between
    1 and 32". Throw the range-validation IllegalArgumentException there
    instead, matching the single-category reachability checks.
---
 .../apache/commons/lang3/RandomStringUtils.java    | 10 +++++++-
 .../commons/lang3/RandomStringUtilsTest.java       | 30 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/commons/lang3/RandomStringUtils.java 
b/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
index 40a5dc744..ba2129e40 100644
--- a/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/RandomStringUtils.java
@@ -296,7 +296,9 @@ public static String random(int count, int start, int end, 
final boolean letters
             if (letters && digits && start <= ASCII_0 && end >= ASCII_z + 1) {
                 return random(count, 0, 0, false, false, ALPHANUMERICAL_CHARS, 
random);
             }
-            if (digits && end <= ASCII_0 || letters && end <= ASCII_A) {
+            // Only reject when none of the requested categories is reachable; 
otherwise a letters && digits
+            // request would throw on a range that holds one category but not 
the other (e.g. [ASCII_0, ASCII_A)).
+            if ((!digits || end <= ASCII_0) && (!letters || end <= ASCII_A) && 
(digits || letters)) {
                 throw new IllegalArgumentException(
                         String.format("Parameter end (%,d) must be greater 
than (%,d) for generating digits or greater than (%,d) for generating 
letters.", end,
                                 ASCII_0, ASCII_A));
@@ -312,6 +314,12 @@ public static String random(int count, int start, int end, 
final boolean letters
             if (letters && digits) {
                 start = Math.max(ASCII_0, start);
                 end = Math.min(ASCII_z + 1, end);
+                // The clamp can empty the range when it sits above the 
alphanumerics (e.g. [ASCII_z + 1, 0x7f)),
+                // unlike the single-category branches below which are 
validated by the reachability loops further
+                // down. Reject here so the caller gets a clear range error 
instead of nextBits(0) failing later.
+                if (start >= end) {
+                    throw new IllegalArgumentException(String.format("No 
letters or digits exist between start %,d and end %,d.", start, end));
+                }
             } else if (digits) {
                 // just numbers, no letters
                 start = Math.max(ASCII_0, start);
diff --git a/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
index 9afe8ad5f..2c6a29181 100644
--- a/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java
@@ -119,6 +119,36 @@ public void testCustomLetterCharsArrayDoesNotThrowIAE() {
         }, "RandomStringUtils.random() threw IAE for valid letter chars array 
- pre-patch behavior");
     }
 
+    /**
+     * Asking for {@code letters && digits} must never be stricter than asking 
for {@code digits} alone. The range
+     * {@code ['0', 'A')} holds the digits but no letters, so {@code 
random(count, '0', 'A', true, true, ...)} must
+     * generate digits like the digits-only call over the same range, not 
throw IllegalArgumentException.
+     */
+    @Test
+    void testLettersAndDigitsOverDigitOnlyRange() {
+        final String both = RandomStringUtils.random(100, '0', 'A', true, 
true, null, new Random(42));
+        assertEquals(100, both.length());
+        for (final char c : both.toCharArray()) {
+            assertTrue(c >= '0' && c <= '9', () -> "Expected a digit but got: 
" + c);
+        }
+        // digits alone already works over this range, so letters && digits 
must not reject it
+        assertDoesNotThrow(() -> RandomStringUtils.random(100, '0', 'A', 
false, true, null, new Random(42)));
+    }
+
+    /**
+     * The {@code letters && digits} ASCII fast path clamps {@code start} up 
to {@code '0'} and {@code end} down to
+     * {@code 'z' + 1}. A range sitting entirely above the alphanumerics, e.g. 
{@code ['z' + 1, 0x7f)}, collapses to
+     * {@code start >= end} after that clamp. It must throw a clear range 
IllegalArgumentException, not fall through to
+     * {@code nextBits(0)} which reports the unrelated "number of bits must be 
between 1 and 32".
+     */
+    @Test
+    void testLettersAndDigitsOverEmptyAsciiRange() {
+        final IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class,
+                () -> RandomStringUtils.random(10, 'z' + 1, 0x7f, true, true, 
null, new Random(42)));
+        assertTrue(e.getMessage() != null && !e.getMessage().contains("number 
of bits"),
+                () -> "Expected a range-validation message but got: " + 
e.getMessage());
+    }
+
     @Test
     void testExceptionsRandom() {
         assertIllegalArgumentException(() -> RandomStringUtils.random(-1));

Reply via email to