From 29fc853271347f74d468fabb7daae240bc84f71e Mon Sep 17 00:00:00 2001 From: Lulu13022002 <41980282+Lulu13022002@users.noreply.github.com> Date: Sat, 21 Jun 2025 07:56:45 +0200 Subject: [PATCH] Allow empty paths in namespaced keys (#12687) --- .../main/java/org/bukkit/NamespacedKey.java | 88 ++++++------------- .../java/org/bukkit/NamespacedKeyTest.java | 10 --- .../bukkit/craftbukkit/CraftLootTable.java | 2 +- .../craftbukkit/util/CraftNamespacedKey.java | 9 -- 4 files changed, 28 insertions(+), 81 deletions(-) diff --git a/paper-api/src/main/java/org/bukkit/NamespacedKey.java b/paper-api/src/main/java/org/bukkit/NamespacedKey.java index 36d8d113df3..09cbe299a79 100644 --- a/paper-api/src/main/java/org/bukkit/NamespacedKey.java +++ b/paper-api/src/main/java/org/bukkit/NamespacedKey.java @@ -1,8 +1,12 @@ package org.bukkit; import com.google.common.base.Preconditions; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Locale; +import java.util.OptionalInt; import java.util.UUID; +import net.kyori.adventure.key.Key; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -11,7 +15,7 @@ import org.jetbrains.annotations.Nullable; /** * Represents a String based key which consists of two components - a namespace * and a key. - * + *
* Namespaces may only contain lowercase alphanumeric characters, periods, * underscores, and hyphens. *
@@ -19,7 +23,7 @@ import org.jetbrains.annotations.Nullable; * underscores, hyphens, and forward slashes. * */ -public final class NamespacedKey implements net.kyori.adventure.key.Key, com.destroystokyo.paper.Namespaced { // Paper - implement Key and Namespaced +public final class NamespacedKey implements Key, com.destroystokyo.paper.Namespaced { /** * The namespace representing all inbuilt keys. @@ -34,44 +38,6 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des private final String namespace; private final String key; - private static boolean isValidNamespaceChar(char c) { - return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; - } - - private static boolean isValidKeyChar(char c) { - return isValidNamespaceChar(c) || c == '/'; - } - - private static boolean isValidNamespace(String namespace) { - int len = namespace.length(); - if (len == 0) { - return false; - } - - for (int i = 0; i < len; i++) { - if (!isValidNamespaceChar(namespace.charAt(i))) { - return false; - } - } - - return true; - } - - private static boolean isValidKey(String key) { - int len = key.length(); - if (len == 0) { - return false; - } - - for (int i = 0; i < len; i++) { - if (!isValidKeyChar(key.charAt(i))) { - return false; - } - } - - return true; - } - /** * Create a key in a specific namespace. *
@@ -115,29 +81,34 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des private void validate() { Preconditions.checkArgument(this.namespace.length() + 1 + this.key.length() <= Short.MAX_VALUE, "NamespacedKey must be less than 32768 characters"); - Preconditions.checkArgument(isValidNamespace(this.namespace), "Invalid namespace. Must be [a-z0-9._-]: %s", this.namespace); - Preconditions.checkArgument(isValidKey(this.key), "Invalid key. Must be [a-z0-9/._-]: %s", this.key); + checkError("[a-z0-9_-.]", "namespace", this.namespace, Key.checkNamespace(this.namespace)); + checkError("[a-z0-9_-./]", "key", this.key, Key.checkValue(this.key)); + } + + private static void checkError(String pattern, String name, String value, OptionalInt index) { + index.ifPresent(indexValue -> { + char character = value.charAt(indexValue); + throw new IllegalArgumentException(String.format("Non %s character in %s '%s' at index %d ('%s', bytes: %s)", pattern, name, value, indexValue, character, Arrays.toString(String.valueOf(character).getBytes(StandardCharsets.UTF_8)))); + }); } @NotNull - @Override // Paper + @Override public String getNamespace() { - return namespace; + return this.namespace; } @NotNull - @Override // Paper + @Override public String getKey() { - return key; + return this.key; } @Override public int hashCode() { - // Paper start int result = this.namespace.hashCode(); result = (31 * result) + this.key.hashCode(); return result; - // Paper end } @Override @@ -145,15 +116,14 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des if (obj == null) { return false; } - // Paper start - if (!(obj instanceof net.kyori.adventure.key.Key key)) return false; + + if (!(obj instanceof Key key)) return false; return this.namespace.equals(key.namespace()) && this.key.equals(key.value()); - // Paper end } @Override public String toString() { - return this.namespace + ":" + this.key; + return this.namespace + ':' + this.key; } /** @@ -206,10 +176,8 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des */ @Nullable public static NamespacedKey fromString(@NotNull String string, @Nullable Plugin defaultNamespace) { - // Paper - Return null for empty string, check length Preconditions.checkArgument(string != null, "Input string must not be null"); if (string.isEmpty() || string.length() > Short.MAX_VALUE) return null; - // Paper end - Return null for empty string, check length String[] components = string.split(":", 3); if (components.length > 2) { @@ -219,12 +187,12 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des String key = (components.length == 2) ? components[1] : ""; if (components.length == 1) { String value = components[0]; - if (value.isEmpty() || !isValidKey(value)) { + if (value.isEmpty() || !Key.parseableValue(value)) { return null; } return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, value) : minecraft(value); - } else if (components.length == 2 && !isValidKey(key)) { + } else if (components.length == 2 && !Key.parseableValue(key)) { return null; } @@ -233,7 +201,7 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des return (defaultNamespace != null) ? new NamespacedKey(defaultNamespace, key) : minecraft(key); } - if (!isValidNamespace(namespace)) { + if (!Key.parseableNamespace(namespace)) { return null; } @@ -242,7 +210,7 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des /** * Get a NamespacedKey from the supplied string. - * + *
* The default namespace will be Minecraft's (i.e.
* {@link #minecraft(String)}).
*
@@ -255,7 +223,6 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des
return fromString(key, null);
}
- // Paper start
@NotNull
@Override
public String namespace() {
@@ -271,7 +238,6 @@ public final class NamespacedKey implements net.kyori.adventure.key.Key, com.des
@NotNull
@Override
public String asString() {
- return this.namespace + ':' + this.key;
+ return this.toString();
}
- // Paper end
}
diff --git a/paper-api/src/test/java/org/bukkit/NamespacedKeyTest.java b/paper-api/src/test/java/org/bukkit/NamespacedKeyTest.java
index 7d81f92008b..c9fb856be75 100644
--- a/paper-api/src/test/java/org/bukkit/NamespacedKeyTest.java
+++ b/paper-api/src/test/java/org/bukkit/NamespacedKeyTest.java
@@ -34,16 +34,6 @@ public class NamespacedKeyTest {
assertThrows(IllegalArgumentException.class, () -> NamespacedKey.fromString(null));
}
- @Test
- public void testEmptyNamespace() {
- assertThrows(IllegalArgumentException.class, () -> new NamespacedKey("", "foo").toString());
- }
-
- @Test
- public void testEmptyKey() {
- assertThrows(IllegalArgumentException.class, () -> new NamespacedKey("minecraft", "").toString());
- }
-
@Test
public void testInvalidNamespace() {
assertThrows(IllegalArgumentException.class, () -> new NamespacedKey("minecraft/test", "foo").toString());
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
index 3cf0f434f37..bac8aaab51f 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftLootTable.java
@@ -39,7 +39,7 @@ public class CraftLootTable implements org.bukkit.loot.LootTable {
}
public static org.bukkit.loot.LootTable minecraftToBukkit(ResourceKey