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

papegaaij pushed a commit to branch resource-name-iterator-alloc
in repository https://gitbox.apache.org/repos/asf/wicket.git


The following commit(s) were added to refs/heads/resource-name-iterator-alloc 
by this push:
     new 88972c93b6 Validate the locale without building its string form
88972c93b6 is described below

commit 88972c93b69ae67b731e497c0fc9233101403780
Author: Emond Papegaaij <[email protected]>
AuthorDate: Sun Sep 6 08:37:43 2026 +0000

    Validate the locale without building its string form
    
    ResourceUtil#rejectPathSeparators(Locale) ran Locale#toString() on every 
call,
    and that builds a new string each time. It runs once per 
ResourceNameIterator
    construction, so three times per property lookup - once per registered
    properties loader.
    
    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 keep the 
general
    route, and that fallback is load-bearing rather than defensive:
    Locale#toString() omits a variant that has neither a language nor a 
country, so
    Locale.of("", "", "a/b") renders as the empty string and must not be 
rejected.
    
    Checked against the previous implementation over 4918 locales - every
    combination of 17 subtag values across language, country and variant, plus
    script and extension shapes and null - with identical results throughout.
    
    ResourceNameIteratorBenchmark, nl_NL without a style:
    
      walkAllCandidates (miss)  167.8 -> 139.9 ns/op   736 -> 672 B/op
      firstCandidate (hit)      421.6 -> 405.6 ns/op   432 -> 368 B/op
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../apache/wicket/resource/ResourceUtilTest.java   | 48 ++++++++++++++++++++++
 .../org/apache/wicket/resource/ResourceUtil.java   | 40 +++++++++++++++++-
 2 files changed, 86 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 071e2447b1..38e1b84057 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,54 @@ class ResourceUtilTest
                assertNull(ResourceUtil.rejectPathSeparators(Locale.of("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(Locale.of("nl"), 
ResourceUtil.rejectPathSeparators(Locale.of("nl")));
+               assertEquals(Locale.of("nl", "NL"), 
ResourceUtil.rejectPathSeparators(Locale.of("nl", "NL")));
+               assertEquals(Locale.of("", "NL"), 
ResourceUtil.rejectPathSeparators(Locale.of("", "NL")));
+
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("nl", 
"N/L")));
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("nl", 
"N\\L")));
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("nl", 
"..")));
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("nl", 
"N\0L")));
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("a\\b", 
"NL")));
+
+               // a single dot is a legal subtag character; only a doubled one 
escapes the directory
+               assertEquals(Locale.of("a.b", "NL"), 
ResourceUtil.rejectPathSeparators(Locale.of("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 = Locale.of("nl", "NL", "vlaams");
+               assertEquals(variant, 
ResourceUtil.rejectPathSeparators(variant));
+               assertNull(ResourceUtil.rejectPathSeparators(Locale.of("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 = Locale.of("", "", "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 82402843e9..76809d96de 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