diff --git a/key/src/main/java/net/kyori/adventure/key/Key.java b/key/src/main/java/net/kyori/adventure/key/Key.java index 33274665f..286ac16cc 100644 --- a/key/src/main/java/net/kyori/adventure/key/Key.java +++ b/key/src/main/java/net/kyori/adventure/key/Key.java @@ -24,6 +24,8 @@ package net.kyori.adventure.key; import java.util.Comparator; +import java.util.OptionalInt; +import java.util.function.IntConsumer; import java.util.stream.Stream; import net.kyori.examination.Examinable; import net.kyori.examination.ExaminableProperty; @@ -174,12 +176,23 @@ static boolean parseable(final @Nullable String string) { * @since 4.12.0 */ static boolean parseableNamespace(final @NotNull String namespace) { + return !checkNamespace(namespace).isPresent(); + } + + /** + * Checks if {@code value} is a valid namespace. + * + * @param namespace the string to check + * @return {@link OptionalInt#empty()} if {@code value} is a valid namespace, otherwise an {@code OptionalInt} containing the index of an invalid character + * @since 4.14.0 + */ + static @NotNull OptionalInt checkNamespace(final @NotNull String namespace) { for (int i = 0, length = namespace.length(); i < length; i++) { if (!allowedInNamespace(namespace.charAt(i))) { - return false; + return OptionalInt.of(i); } } - return true; + return OptionalInt.empty(); } /** @@ -190,12 +203,23 @@ static boolean parseableNamespace(final @NotNull String namespace) { * @since 4.12.0 */ static boolean parseableValue(final @NotNull String value) { + return !checkValue(value).isPresent(); + } + + /** + * Checks if {@code value} is a valid value. + * + * @param value the string to check + * @return {@link OptionalInt#empty()} if {@code value} is a valid value, otherwise an {@code OptionalInt} containing the index of an invalid character + * @since 4.14.0 + */ + static @NotNull OptionalInt checkValue(final @NotNull String value) { for (int i = 0, length = value.length(); i < length; i++) { if (!allowedInValue(value.charAt(i))) { - return false; + return OptionalInt.of(i); } } - return true; + return OptionalInt.empty(); } /** diff --git a/key/src/main/java/net/kyori/adventure/key/KeyImpl.java b/key/src/main/java/net/kyori/adventure/key/KeyImpl.java index c05c6d0b9..b7227a1bb 100644 --- a/key/src/main/java/net/kyori/adventure/key/KeyImpl.java +++ b/key/src/main/java/net/kyori/adventure/key/KeyImpl.java @@ -23,8 +23,11 @@ */ package net.kyori.adventure.key; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Comparator; import java.util.Objects; +import java.util.OptionalInt; import java.util.stream.Stream; import net.kyori.examination.ExaminableProperty; import org.jetbrains.annotations.NotNull; @@ -41,12 +44,27 @@ final class KeyImpl implements Key { private final String value; KeyImpl(final @NotNull String namespace, final @NotNull String value) { - if (!Key.parseableNamespace(namespace)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9_.-] character in namespace of Key[%s]", asString(namespace, value))); - if (!Key.parseableValue(value)) throw new InvalidKeyException(namespace, value, String.format("Non [a-z0-9/._-] character in value of Key[%s]", asString(namespace, value))); + checkError("namespace", namespace, value, Key.checkNamespace(namespace)); + checkError("value", namespace, value, Key.checkValue(value)); this.namespace = requireNonNull(namespace, "namespace"); this.value = requireNonNull(value, "value"); } + private static void checkError(final String name, final String namespace, final String value, final OptionalInt index) { + if (index.isPresent()) { + final int indexValue = index.getAsInt(); + final char character = value.charAt(indexValue); + throw new InvalidKeyException(namespace, value, String.format( + "Non [a-z0-9_.-] character in %s of Key[%s] at index %d ('%s', bytes: %s)", + name, + asString(namespace, value), + indexValue, + character, + Arrays.toString(String.valueOf(character).getBytes(StandardCharsets.UTF_8)) + )); + } + } + static boolean allowedInNamespace(final char character) { return character == '_' || character == '-' || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '.'; } diff --git a/key/src/test/java/net/kyori/adventure/key/KeyTest.java b/key/src/test/java/net/kyori/adventure/key/KeyTest.java index 021b80420..9e6fb64e9 100644 --- a/key/src/test/java/net/kyori/adventure/key/KeyTest.java +++ b/key/src/test/java/net/kyori/adventure/key/KeyTest.java @@ -107,4 +107,9 @@ void testParseableValue() { assertTrue(Key.parseableValue("empty")); assertTrue(Key.parseableValue("some/path")); } + + @Test + void testNulChar() { + assertThrows(InvalidKeyException.class, () -> Key.key("carbon:global\0\0\0")); + } }