Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions user/src/com/google/gwt/i18n/server/GwtLocaleFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ private static boolean isDigit(String str, int min, int max) {
return matches(str, min, max, false);
}

private static boolean isAlphaNumeric(String str, int min, int max) {
int len = str.length();
if (len < min || len > max) {
return false;
}
for (int i = 0; i < len; ++i) {
char c = str.charAt(i);
// BCP47 subtags are ASCII only, so avoid Character.isLetterOrDigit which
// would also accept non-ASCII letters and digits.
boolean alphaNumeric = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9');
if (!alphaNumeric) {
return false;
}
}
return true;
}

/**
* Check if the supplied string matches length and composition requirements.
*
Expand Down Expand Up @@ -143,6 +161,14 @@ public GwtLocale fromString(String localeName) {
ArrayList<String> localeParts = new ArrayList<String>();
String[] parts = localeName.split("[-_]");
for (int i = 0; i < parts.length; ++i) {
// The split only breaks on '-'/'_', so any other character stays inside
// a part. Require each subtag to be alphanumeric and 1-8 chars, matching
// BCP47, so an untrusted server-side locale can't smuggle '.', '$' or
// '/' into the class name LocalizableInstantiator resolves reflectively.
if (!isAlphaNumeric(parts[i], 1, 8)) {
throw new IllegalArgumentException("Unrecognized locale format: "
+ localeName);
}
if (parts[i].length() == 1 && i + 1 < parts.length) {
localeParts.add(parts[i] + '-' + parts[++i]);
} else {
Expand Down
22 changes: 22 additions & 0 deletions user/test/com/google/gwt/i18n/server/GwtLocaleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.gwt.i18n.server;

import static com.google.gwt.testing.server.Assertions.assertThrows;

import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.i18n.shared.GwtLocaleFactory;

Expand Down Expand Up @@ -174,6 +176,26 @@ public void testFromString() {
}
}

public void testFromStringRejectsInvalidLanguage() {
// The language subtag is concatenated into class names resolved reflectively
// on the server, so it must not carry characters like '.', '$' or '/'.
String[] invalid = {
"com.google.gwt.dev.Compiler", "java.lang.Runtime", "x$Evil", "a/b/c",
"en.US",
};
for (String locale : invalid) {
assertThrows(IllegalArgumentException.class,
() -> factory.fromString(locale),
"Should have thrown IllegalArgumentException on " + locale);
}
// Well-formed tags, including extended-language and private-use forms, are
// still accepted with the language preserved verbatim.
assertEquals("en", factory.fromString("en_US").getLanguage());
assertEquals("zh-cmn", factory.fromString("zh-cmn").getLanguage());
assertEquals("i-klingon", factory.fromString("i-klingon").getLanguage());
assertEquals("x-foo123", factory.fromString("x-foo123").getLanguage());
}

public void testInheritance() {
GwtLocale en = factory.fromString("en_Latn_US_VARIANT");
List<GwtLocale> chain = en.getInheritanceChain();
Expand Down
6 changes: 5 additions & 1 deletion user/test/com/google/gwt/testing/TestUtils.gwt.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
<!-- limitations under the License. -->

<module>
<source path='' />
<!-- The server subpackage holds JVM-only test helpers and must stay out of
the translatable source path. -->
<source path=''>
<exclude name='server/**' />
</source>
</module>
52 changes: 52 additions & 0 deletions user/test/com/google/gwt/testing/server/Assertions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2026 GWT Project Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.testing.server;

/**
* Assertion helpers for server-side JUnit 3 tests, where the JUnit 4/5
* {@code assertThrows} is not available.
*/
public class Assertions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still in a client package despite being in a package named "server" - tests are failing, but only the one suite that uses the testing package

Tracing compile failure path for type 'com.google.gwt.testing.server.Assertions'
   [ERROR] Errors in 'file:/home/runner/work/gwt/gwt/gwt/user/test/com/google/gwt/testing/server/Assertions.java'
      [ERROR] Line 42: The method isInstance(Throwable) is undefined for the type Class<T>
      [ERROR] Line 43: The method cast(Throwable) is undefined for the type Class<T>
[ERROR] Aborting compile due to errors in some input files
Tracing compile failure path for type 'com.google.gwt.testing.server.Assertions'
   [ERROR] Errors in 'file:/home/runner/work/gwt/gwt/gwt/user/test/com/google/gwt/testing/server/Assertions.java'
      [ERROR] Line 42: The method isInstance(Throwable) is undefined for the type Class<T>
      [ERROR] Line 43: The method cast(Throwable) is undefined for the type Class<T>
[ERROR] Aborting compile due to errors in some input files

Worth nothing, this isnt "server" code anyway, just jvm code, but if you want a class like this, it needs to be in a non-client package. Or, if you are okay with it working in GWT too, get rid of the isInstance/cast usage so reflection isn't required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the point of the class was the Jupiter-equivalent semantics for JVM tests, I kept isInstance/cast and took the package out of the translatable path instead: TestUtils.gwt.xml now excludes server/** from its source, so the GWT compiler no longer sees it and the suite that inherits the testing module compiles again. Can move it to a different package outright if you'd rather not carry the exclude.


/**
* A block of code that may throw any {@link Throwable}, so it can be passed to
* {@link #assertThrows} as a lambda.
*/
public interface Executable {
void execute() throws Throwable;
}

/**
* Asserts that running {@code executable} throws an exception of
* {@code expectedType} (or a subtype) and returns it, failing with
* {@code message} if nothing is thrown or a different type is thrown.
*/
public static <T extends Throwable> T assertThrows(Class<T> expectedType,
Executable executable, String message) {
try {
executable.execute();
} catch (Throwable actual) {
if (expectedType.isInstance(actual)) {
return expectedType.cast(actual);
}
throw new AssertionError(message + " but threw " + actual, actual);
}
throw new AssertionError(message);
}

private Assertions() {
}
}
Loading