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 d2e720ec2 LocaleUtils static caches no longer grows on invalid input 
to LocaleUtils.countriesByLanguage(String) (f007).
d2e720ec2 is described below

commit d2e720ec2c8abb54c0dc3ebe6985a1d1b288d7e0
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 07:32:34 2026 -0400

    LocaleUtils static caches no longer grows on invalid input to
    LocaleUtils.countriesByLanguage(String) (f007).
---
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/commons/lang3/LocaleUtils.java | 35 +++++++++++++++++-----
 .../org/apache/commons/lang3/LocaleUtilsTest.java  | 12 ++++++++
 3 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9d960ce8a..2550f2532 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -255,6 +255,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">UnicodeUnescaper.unescapeJava/EcmaScript/Json: malformed \u sequences 
throw undeclared IllegalArgumentException, AND non-ASCII digit spellings of \u 
escapes are silently accepted; both arms of one missing ASCII-hex prescan 
(f004).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StringUtils.replaceEachRepeatedly derives its recursion budget from 
the input itself; the documented StackOverflowError protection fails on large 
tables, and expanding rules amplify text 64x even at the default TTL 
(f005).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">EventUtils.EventBindingInvocationHandler.invoke() dispatches 
Object.hashCode/equals/toString into the bound business method 
(empty-eventTypes default), or returns null -> NPE from hash collections 
(non-empty) (f006).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">LocaleUtils static caches no longer grows on invalid input to 
LocaleUtils.countriesByLanguage(String) (f007).</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/LocaleUtils.java 
b/src/main/java/org/apache/commons/lang3/LocaleUtils.java
index 2d8eb0c8a..9b5947481 100644
--- a/src/main/java/org/apache/commons/lang3/LocaleUtils.java
+++ b/src/main/java/org/apache/commons/lang3/LocaleUtils.java
@@ -83,15 +83,15 @@ private static final class SyncAvoid {
     /**
      * Concurrent map of language locales by country.
      */
-    private static final ConcurrentMap<String, List<Locale>> 
cLanguagesByCountry = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, List<Locale>> ccToLocalesMap = 
new ConcurrentHashMap<>();
 
     /**
      * Concurrent map of country locales by language.
      */
-    private static final ConcurrentMap<String, List<Locale>> 
cCountriesByLanguage = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<String, List<Locale>> lcToLocalesMap = 
new ConcurrentHashMap<>();
 
     /**
-     * Obtains an unmodifiable and sorted list of installed locales.
+     * Gets an unmodifiable and sorted list of installed locales.
      *
      * <p>
      * This method is a wrapper around {@link Locale#getAvailableLocales()}. 
It is more efficient, as the JDK method must create a new array each time it is
@@ -109,7 +109,7 @@ private static List<Locale> availableLocaleList(final 
Predicate<Locale> predicat
     }
 
     /**
-     * Obtains an unmodifiable set of installed locales.
+     * Gets an unmodifiable set of installed locales.
      *
      * <p>
      * This method is a wrapper around {@link Locale#getAvailableLocales()}. 
It is more efficient, as the JDK method must create a new array each time it is
@@ -123,7 +123,7 @@ public static Set<Locale> availableLocaleSet() {
     }
 
     /**
-     * Obtains the list of countries supported for a given language.
+     * Gets the list of countries supported for a given language.
      *
      * <p>
      * This method takes a language code and searches to find the countries 
available for that language. Variant locales are removed.
@@ -136,10 +136,25 @@ public static List<Locale> countriesByLanguage(final 
String languageCode) {
         if (languageCode == null) {
             return Collections.emptyList();
         }
-        return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> 
Collections
+        // Only syntactically valid ISO 639 codes can match an available 
locale's language; anything
+        // else is answered without touching the cache so that arbitrary 
caller strings are never
+        // retained for the lifetime of the class loader.
+        if (!languageCode.isEmpty() && !isISO639LanguageCode(languageCode)) {
+            return Collections.emptyList();
+        }
+        return lcToLocalesMap.computeIfAbsent(languageCode, lc -> Collections
                 .unmodifiableList(availableLocaleList(locale -> 
languageCode.equals(locale.getLanguage()) && !hasCountry(locale) && 
hasVariant(locale))));
     }
 
+    /**
+     * Gets the cache of country locales by language.
+     *
+     * @return the cache of country locales by language.
+     */
+    static ConcurrentMap<String, List<Locale>> getLcToLocalesMap() {
+        return lcToLocalesMap;
+    }
+
     /**
      * Tests whether the given Locale defines a variant.
      *
@@ -250,7 +265,13 @@ public static List<Locale> languagesByCountry(final String 
countryCode) {
         if (countryCode == null) {
             return Collections.emptyList();
         }
-        return cLanguagesByCountry.computeIfAbsent(countryCode,
+        // Only syntactically valid ISO 3166 alpha-2 / UN M.49 numeric codes 
can match an available
+        // locale's country; anything else is answered without touching the 
cache so that arbitrary
+        // caller strings are never retained for the lifetime of the JVM.
+        if (!countryCode.isEmpty() && !isISO3166CountryCode(countryCode) && 
!isNumericAreaCode(countryCode)) {
+            return Collections.emptyList();
+        }
+        return ccToLocalesMap.computeIfAbsent(countryCode,
                 k -> Collections.unmodifiableList(availableLocaleList(locale 
-> countryCode.equals(locale.getCountry()) && hasVariant(locale))));
     }
 
diff --git a/src/test/java/org/apache/commons/lang3/LocaleUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/LocaleUtilsTest.java
index c54740a7b..5b086766c 100644
--- a/src/test/java/org/apache/commons/lang3/LocaleUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/LocaleUtilsTest.java
@@ -40,7 +40,9 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EmptySource;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.junitpioneer.jupiter.DefaultLocale;
 import org.junitpioneer.jupiter.ReadsDefaultLocale;
 
@@ -258,6 +260,16 @@ void testCountriesByLanguage() {
         assertCountriesByLanguage("it", new String[]{"IT", "CH"});
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"x", "abcd", "EN", "e1", "en-US", " "})
+    void testCountriesByLanguageDoesNotCacheInvalidLanguageCode(final String 
languageCode) {
+        assertFalse(LocaleUtils.getLcToLocalesMap().containsKey(languageCode));
+        final int cacheSize = LocaleUtils.getLcToLocalesMap().size();
+        assertTrue(LocaleUtils.countriesByLanguage(languageCode).isEmpty());
+        assertEquals(cacheSize, LocaleUtils.getLcToLocalesMap().size());
+        assertFalse(LocaleUtils.getLcToLocalesMap().containsKey(languageCode));
+    }
+
     @Test
     void testIllegalLanguageWithNumericCountry() {
         assertIllegalArgumentException(() -> 
LocaleUtils.toLocale("../../unexpected_001"));

Reply via email to