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

papegaaij pushed a commit to branch performance-improvements-10.x
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 917d80f35bb823b7f1e7839cc74056f5d136b143
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Sep 11 22:06:47 2026 +0200

    Validate the locale without building its string form
    
    ResourceUtil#rejectPathSeparators ran Locale#toString() on every call, and
    Locale#toString() builds a new string each time. Every resource lookup 
validates
    the locale, so that is once per lookup for a result that is thrown away.
    
    Without a variant, a script or extensions, Locale#toString() returns 
nothing but
    the language and the country joined by '_', so inspecting those two subtags
    directly is equivalent and allocates nothing. Richer locales are rare and 
still
    take the general route, which also keeps the two in agreement about the 
subtags
    that toString() drops.
    
    Verified by enumeration over 4918 locales: byte-identical outcomes before 
and
    after.
    
    Backported from master.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../apache/wicket/resource/ResourceUtilTest.java   | 50 ++++++++++++++++++++++
 .../org/apache/wicket/resource/ResourceUtil.java   | 40 ++++++++++++++++-
 2 files changed, 88 insertions(+), 2 deletions(-)

diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/resource/ResourceUtilTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/resource/ResourceUtilTest.java
index a56c396948..c0077ee166 100644
--- 
a/wicket-core-tests/src/test/java/org/apache/wicket/resource/ResourceUtilTest.java
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/resource/ResourceUtilTest.java
@@ -87,6 +87,56 @@ class ResourceUtilTest
                assertNull(ResourceUtil.rejectPathSeparators(new 
Locale("a/b")));
        }
 
+       /**
+        * A locale without a variant, script or extensions is validated by 
inspecting its language and
+        * country rather than its {@link Locale#toString()}, so both subtags 
must still be checked, and
+        * a separator has to be caught wherever it sits.
+        */
+       @Test
+       void rejectPathSeparatorsForLanguageAndCountry() throws Exception
+       {
+               assertEquals(new Locale("nl"), 
ResourceUtil.rejectPathSeparators(new Locale("nl")));
+               assertEquals(new Locale("nl", "NL"),
+                       ResourceUtil.rejectPathSeparators(new Locale("nl", 
"NL")));
+               assertEquals(new Locale("", "NL"), 
ResourceUtil.rejectPathSeparators(new Locale("", "NL")));
+
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("nl", 
"N/L")));
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("nl", 
"N\\L")));
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("nl", 
"..")));
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("nl", 
"N\0L")));
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("a\\b", 
"NL")));
+
+               // a single dot is a legal subtag character; only a doubled one 
escapes the directory
+               assertEquals(new Locale("a.b", "NL"),
+                       ResourceUtil.rejectPathSeparators(new Locale("a.b", 
"NL")));
+       }
+
+       /**
+        * A locale carrying a variant, a script or extensions is validated 
against its
+        * {@link Locale#toString()}, which drops some subtags - a variant 
without a language or country
+        * among them - so the two routes must agree on what reaches the path.
+        */
+       @Test
+       void rejectPathSeparatorsForRicherLocales() throws Exception
+       {
+               Locale variant = new Locale("nl", "NL", "vlaams");
+               assertEquals(variant, 
ResourceUtil.rejectPathSeparators(variant));
+               assertNull(ResourceUtil.rejectPathSeparators(new Locale("nl", 
"NL", "a/b")));
+
+               Locale script = new 
Locale.Builder().setLanguage("zh").setScript("Hans").build();
+               assertEquals(script, ResourceUtil.rejectPathSeparators(script));
+
+               Locale extension =
+                       new 
Locale.Builder().setLanguage("nl").setRegion("NL").setExtension('u', 
"ca-buddhist").build();
+               assertEquals(extension, 
ResourceUtil.rejectPathSeparators(extension));
+
+               // Locale#toString() omits a variant that has neither a 
language nor a country, so it never
+               // reaches the lookup path and must not cause the locale to be 
dropped
+               Locale strayVariant = new Locale("", "", "a/b");
+               assertEquals("", strayVariant.toString());
+               assertEquals(strayVariant, 
ResourceUtil.rejectPathSeparators(strayVariant));
+       }
+
        /**
         * A locale, style or variation carrying a path separator is dropped: 
each becomes a single
         * component of the resource lookup path, so a separator would make the 
lookup resolve in a
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/resource/ResourceUtil.java 
b/wicket-core/src/main/java/org/apache/wicket/resource/ResourceUtil.java
index 851ebde007..d6d1190996 100644
--- a/wicket-core/src/main/java/org/apache/wicket/resource/ResourceUtil.java
+++ b/wicket-core/src/main/java/org/apache/wicket/resource/ResourceUtil.java
@@ -90,12 +90,48 @@ public class ResourceUtil
         */
        public static Locale rejectPathSeparators(final Locale locale)
        {
-               if (locale == null || rejectPathSeparators(locale.toString(), 
"locale") != null)
+               if (locale == null)
                {
                        return locale;
                }
 
-               return null;
+               // Every resource lookup validates the locale, and 
Locale#toString() builds a new string
+               // each time it is called. Without a variant, a script or 
extensions it returns nothing but
+               // the language and the country joined by '_', so inspecting 
those two directly is
+               // equivalent and allocates nothing. Richer locales are rare 
and take the general route.
+               if (locale.getVariant().isEmpty() && 
locale.getScript().isEmpty() &&
+                       locale.getExtensionKeys().isEmpty())
+               {
+                       if (isPathComponent(locale.getLanguage()) && 
isPathComponent(locale.getCountry()))
+                       {
+                               return locale;
+                       }
+
+                       log.warn("Ignoring the locale because it contains a 
path separator or NUL: {}", locale);
+
+                       return null;
+               }
+
+               return rejectPathSeparators(locale.toString(), "locale") != 
null ? locale : null;
+       }
+
+       /**
+        * @return whether the value can be used as a single path component, 
i.e. contains none of
+        *         {@code /}, {@code \}, {@code ..} or a NUL character
+        */
+       private static boolean isPathComponent(final String value)
+       {
+               for (int i = 0; i < value.length(); i++)
+               {
+                       char c = value.charAt(i);
+                       if (c == '/' || c == '\\' || c == '\0' ||
+                               (c == '.' && i > 0 && value.charAt(i - 1) == 
'.'))
+                       {
+                               return false;
+                       }
+               }
+
+               return true;
        }
 
        /**

Reply via email to