8385834: Tighten ListFormat.getInstance(String[]) behavior for invalid placeholders

Reviewed-by: jlu
This commit is contained in:
Naoto Sato 2026-06-08 16:28:15 +00:00
parent 5787c6b3d7
commit 177a371109
2 changed files with 96 additions and 34 deletions

View File

@ -84,9 +84,9 @@ import sun.util.locale.provider.LocaleProviderAdapter;
* Note: these examples are from CLDR, there could be different results from other locale providers.
* <p>
* Alternatively, Locale, Type, and/or Style independent instances
* can be created with {@link #getInstance(String[])}. The String array to the
* method specifies the delimiting patterns for the start/middle/end portion of
* the formatted string, as well as optional specialized patterns for two or three
* can be created with {@link #getInstance(String[])}. The String array passed to the
* method specifies the delimiting patterns for the {@code start}/{@code middle}/{@code end}
* portion of the formatted string, as well as optional specialized patterns for two or three
* elements. Refer to the method description for more detail.
* <p>
* On parsing, if some ambiguity is found in the input string, such as delimiting
@ -121,7 +121,8 @@ public final class ListFormat extends Format {
/**
* The array of five pattern Strings. Each element corresponds to the Unicode LDML's
* `listPatternsPart` type, i.e, start/middle/end/two/three.
* {@code listPatternPart} type, i.e,
* {@code start}/{@code middle}/{@code end}/{@code two}/{@code three}.
* @serial
*/
private final String[] patterns;
@ -153,6 +154,7 @@ public final class ListFormat extends Format {
var pattern = patterns[START];
var placeholderPositions = findPlaceholders(pattern);
if (placeholderPositions != null &&
placeholderPositions[2] == -1 &&
placeholderPositions[1] + PLACEHOLDER_LENGTH == pattern.length()) {
startBefore = pattern.substring(0, placeholderPositions[0]);
startBetween = pattern.substring(placeholderPositions[0] + PLACEHOLDER_LENGTH,
@ -164,6 +166,7 @@ public final class ListFormat extends Format {
pattern = patterns[MIDDLE];
placeholderPositions = findPlaceholders(pattern);
if (placeholderPositions != null &&
placeholderPositions[2] == -1 &&
placeholderPositions[0] == 0 &&
placeholderPositions[1] + PLACEHOLDER_LENGTH == pattern.length()) {
middleBetween = pattern.substring(placeholderPositions[0] + PLACEHOLDER_LENGTH,
@ -174,7 +177,9 @@ public final class ListFormat extends Format {
pattern = patterns[END];
placeholderPositions = findPlaceholders(pattern);
if (placeholderPositions != null && placeholderPositions[0] == 0) {
if (placeholderPositions != null &&
placeholderPositions[2] == -1 &&
placeholderPositions[0] == 0) {
endBetween = pattern.substring(placeholderPositions[0] + PLACEHOLDER_LENGTH,
placeholderPositions[1]);
endAfter = pattern.substring(placeholderPositions[1] + PLACEHOLDER_LENGTH);
@ -185,7 +190,8 @@ public final class ListFormat extends Format {
// Validate two/three patterns, if given. Otherwise, generate them
pattern = patterns[TWO];
if (!pattern.isEmpty()) {
if (findPlaceholders(pattern) == null) {
placeholderPositions = findPlaceholders(pattern);
if (placeholderPositions == null || placeholderPositions[2] >= 0) {
throw new IllegalArgumentException("pattern for two is incorrect: " + pattern);
}
} else {
@ -245,36 +251,43 @@ public final class ListFormat extends Format {
* instead of letting the runtime provide appropriate patterns for the {@code Locale},
* {@code Type}, or {@code Style}.
* <p>
* The patterns array should contain five String patterns, each corresponding to the Unicode LDML's
* {@code listPatternPart}, i.e., "start", "middle", "end", two element, and three element patterns
* in this order. Each pattern contains "{0}" and "{1}" (and "{2}" for the three element pattern)
* placeholders that are substituted with the passed input strings on formatting.
* If the length of the patterns array is not 5, an {@code IllegalArgumentException}
* is thrown.
* The patterns array should contain five String patterns, each corresponding
* to the Unicode LDML's {@code listPatternPart}, i.e., {@code start},
* {@code middle}, {@code end}, {@code two} element, and {@code three}
* element patterns in this order. Each pattern contains "{0}" and "{1}"
* (and "{2}" for the {@code three} element pattern) placeholders that are
* substituted with the passed input strings on formatting. If the length of
* the patterns array is not 5, an {@code IllegalArgumentException} is thrown.
* <p>
* Each pattern string is first parsed as follows. Literals in parentheses, such as
* "start_before", are optional:
* <blockquote><pre>
* {@snippet :
* start := (start_before){0}start_between{1}
* middle := {0}middle_between{1}
* end := {0}end_between{1}(end_after)
* two := (two_before){0}two_between{1}(two_after)
* three := (three_before){0}three_between1{1}three_between2{2}(three_after)
* </pre></blockquote>
* If two or three pattern string is empty, it falls back to
* {@code "(start_before){0}end_between{1}(end_after)"},
* {@code "(start_before){0}start_between{1}end_between{2}(end_after)"} respectively.
* If parsing of any pattern string for start, middle, end, two, or three fails,
* }
* If the {@code two} or {@code three} pattern string is empty, it falls back to
* {@snippet :
* (start_before){0}end_between{1}(end_after)
* (start_before){0}start_between{1}end_between{2}(end_after)
* }
* respectively.
* If parsing of any pattern string for {@code start}, {@code middle},
* {@code end}, {@code two}, or {@code three} fails, including duplicate
* placeholders, "{2}" in patterns other than the {@code three} element
* pattern, or any use of "{" or "}" other than "{0}", "{1}", or "{2}",
* it throws an {@code IllegalArgumentException}.
* <p>
* On formatting, the input string list with {@code n} elements substitutes above
* placeholders based on the number of elements:
* <blockquote><pre>
* {@snippet :
* n = 1: {0}
* n = 2: parsed pattern for "two"
* n = 3: parsed pattern for "three"
* n > 3: (start_before){0}start_between{1}middle_between{2} ... middle_between{m}end_between{n}(end_after)
* </pre></blockquote>
* }
* As an example, the following table shows a pattern array which is equivalent to
* {@code STANDARD} type, {@code FULL} style in US English:
* <table class="striped">
@ -664,15 +677,37 @@ public final class ListFormat extends Format {
/**
* {@return the positions of the "{0}", "{1}", and "{2}" placeholders in the
* given pattern string, or null if the pattern is invalid}
* Only "{0}", "{1}", or "{2}" placeholders are allowed. Any other use of
* curly braces is not allowed.
*
* The returned array contains -1 for "{2}" if that placeholder is absent.
*
* @param pattern pattern string to parse
*/
private static int[] findPlaceholders(String pattern) {
var positions = new int[3];
for (int i = 0; i < positions.length; i++) {
positions[i] = pattern.indexOf("{" + i + "}");
var positions = new int[] {-1, -1, -1};
for (int i = 0; i < pattern.length(); i++) {
var ch = pattern.charAt(i);
if (ch == '{') {
if (i + PLACEHOLDER_LENGTH > pattern.length() ||
pattern.charAt(i + 1) < '0' ||
pattern.charAt(i + 1) > '2' ||
pattern.charAt(i + 2) != '}') {
return null;
}
// Check for duplicate placeholders
var index = pattern.charAt(i + 1) - '0';
if (positions[index] != -1) {
return null;
}
positions[index] = i;
i += PLACEHOLDER_LENGTH - 1;
} else if (ch == '}') {
return null;
}
}
// Check the existence and order of the placeholders

View File

@ -23,7 +23,7 @@
/*
* @test
* @bug 8041488 8316974 8318569 8306116 8385736
* @bug 8041488 8316974 8318569 8306116 8385736 8385834
* @summary Tests for ListFormat class
* @run junit TestListFormat
*/
@ -210,6 +210,10 @@ public class TestListFormat {
arguments(CUSTOM_PATTERNS_MINIMAL, SAMPLE2),
arguments(CUSTOM_PATTERNS_MINIMAL, SAMPLE3),
arguments(CUSTOM_PATTERNS_MINIMAL, SAMPLE4),
arguments(CUSTOM_PATTERNS_METACHAR, SAMPLE1),
arguments(CUSTOM_PATTERNS_METACHAR, SAMPLE2),
arguments(CUSTOM_PATTERNS_METACHAR, SAMPLE3),
arguments(CUSTOM_PATTERNS_METACHAR, SAMPLE4),
};
}
@ -237,13 +241,37 @@ public class TestListFormat {
};
}
private static Arguments[] getInstance_1Arg_InvalidLongPattern() {
private static final String ZERO_REPEAT = "{0}".repeat(100_000);
private static Arguments[] getInstance_1Arg_InvalidPlaceholder() {
return new Arguments[] {
arguments(0, "start pattern is incorrect:"),
arguments(1, "middle pattern is incorrect:"),
arguments(2, "end pattern is incorrect:"),
arguments(3, "pattern for two is incorrect:"),
arguments(4, "pattern for three is incorrect:"),
// Duplicate placeholders
arguments(0, "{0} {0} {1}", "start pattern is incorrect: {0} {0} {1}"),
arguments(0, "{0} {1} {1}", "start pattern is incorrect: {0} {1} {1}"),
arguments(0, "{0} {1} {2}", "start pattern is incorrect: {0} {1} {2}"),
arguments(1, "{0} {0} {1}", "middle pattern is incorrect: {0} {0} {1}"),
arguments(1, "{0} {1} {1}", "middle pattern is incorrect: {0} {1} {1}"),
arguments(1, "{0} {1} {2}", "middle pattern is incorrect: {0} {1} {2}"),
arguments(2, "{0} {0} {1}", "end pattern is incorrect: {0} {0} {1}"),
arguments(2, "{0} {1} {1}", "end pattern is incorrect: {0} {1} {1}"),
arguments(2, "{0} {1} {2}", "end pattern is incorrect: {0} {1} {2}"),
arguments(3, "{0} {0} {1}", "pattern for two is incorrect: {0} {0} {1}"),
arguments(3, "{0} {1} {1}", "pattern for two is incorrect: {0} {1} {1}"),
arguments(3, "{0} {1} {2}", "pattern for two is incorrect: {0} {1} {2}"),
arguments(4, "{0} {2} {1}", "pattern for three is incorrect: {0} {2} {1}"),
arguments(4, "{0} {0} {1} {2}", "pattern for three is incorrect: {0} {0} {1} {2}"),
arguments(4, "{0} {1} {1} {2}", "pattern for three is incorrect: {0} {1} {1} {2}"),
arguments(4, "{0} {1} {2} {2}", "pattern for three is incorrect: {0} {1} {2} {2}"),
arguments(4, ZERO_REPEAT + " {1} {2}", "pattern for three is incorrect: " + ZERO_REPEAT + " {1} {2}"),
// invalid placeholders
arguments(0, "{0} {1} {", "start pattern is incorrect: {0} {1} {"),
arguments(0, "{0} {1} }", "start pattern is incorrect: {0} {1} }"),
arguments(3, "{0} {1} {3}", "pattern for two is incorrect: {0} {1} {3}"),
arguments(4, "{3} {0} {1}", "pattern for three is incorrect: {3} {0} {1}"),
arguments(4, "{333} {0} {1}", "pattern for three is incorrect: {333} {0} {1}"),
arguments(4, "{0} {1} {abc}", "pattern for three is incorrect: {0} {1} {abc}"),
arguments(4, "{0} {1} {2, number}", "pattern for three is incorrect: {0} {1} {2, number}"),
arguments(4, "{0} {1} {2} {3}", "pattern for three is incorrect: {0} {1} {2} {3}"),
};
}
@ -264,7 +292,7 @@ public class TestListFormat {
@ParameterizedTest
@MethodSource
void getInstance_1Arg_InvalidLongPattern(int index, String expected) {
void getInstance_1Arg_InvalidPlaceholder(int index, String invalidPattern, String expected) {
var patterns = new String[]{
"{0}, {1}",
"{0}, {1}",
@ -272,13 +300,12 @@ public class TestListFormat {
"{0} and {1}",
"{0} {1} {2}"
};
patterns[index] = "{0}".repeat(100_000);
patterns[index] = invalidPattern;
// Ensures validation of invalid long patterns completes without timing out
var msg = assertThrows(IllegalArgumentException.class,
() -> ListFormat.getInstance(patterns))
.getMessage();
assertEquals(expected, msg.substring(0, Math.min(msg.length(), expected.length())));
assertEquals(expected, msg);
}
@ParameterizedTest