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"));