diff --git a/src/java.base/share/classes/java/text/ListFormat.java b/src/java.base/share/classes/java/text/ListFormat.java index 3f320bbcc9b..29e58ea717c 100644 --- a/src/java.base/share/classes/java/text/ListFormat.java +++ b/src/java.base/share/classes/java/text/ListFormat.java @@ -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. *

* 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. *

* 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}. *

- * 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. *

* Each pattern string is first parsed as follows. Literals in parentheses, such as * "start_before", are optional: - *

+     * {@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)
-     * 
- * 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}. *

* On formatting, the input string list with {@code n} elements substitutes above * placeholders based on the number of elements: - *

+     * {@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)
-     * 
+ * } * As an example, the following table shows a pattern array which is equivalent to * {@code STANDARD} type, {@code FULL} style in US English: * @@ -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 diff --git a/test/jdk/java/text/Format/ListFormat/TestListFormat.java b/test/jdk/java/text/Format/ListFormat/TestListFormat.java index 1500a1b0818..68da9ce5fda 100644 --- a/test/jdk/java/text/Format/ListFormat/TestListFormat.java +++ b/test/jdk/java/text/Format/ListFormat/TestListFormat.java @@ -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