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;
}
/**