diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java index 48c89c429..5df968930 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java @@ -534,7 +534,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { } } - Map nbtMap = SerializableMeta.getObject(Map.class, map, BUKKIT_CUSTOM_TAG.BUKKIT, true); + Object nbtMap = SerializableMeta.getObject(Object.class, map, BUKKIT_CUSTOM_TAG.BUKKIT, true); // We read both legacy maps and potential modern snbt strings here if (nbtMap != null) { this.persistentDataContainer.putAll((NBTTagCompound) CraftNBTTagConfigSerializer.deserialize(nbtMap)); } diff --git a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java index 754fa7959..54edfead0 100644 --- a/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java +++ b/src/main/java/org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java @@ -153,7 +153,7 @@ public class CraftPersistentDataContainer implements PersistentDataContainer { return hashCode; } - public Map serialize() { - return (Map) CraftNBTTagConfigSerializer.serialize(toTagCompound()); + public String serialize() { + return CraftNBTTagConfigSerializer.serialize(toTagCompound()); } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java b/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java index dd78d2857..b7a8a4ea6 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftNBTTagConfigSerializer.java @@ -15,6 +15,8 @@ import net.minecraft.nbt.NBTTagDouble; import net.minecraft.nbt.NBTTagInt; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; +import net.minecraft.nbt.SnbtPrinterTagVisitor; +import org.jetbrains.annotations.NotNull; public class CraftNBTTagConfigSerializer { @@ -23,35 +25,29 @@ public class CraftNBTTagConfigSerializer { private static final Pattern DOUBLE = Pattern.compile("[-+]?(?:[0-9]+[.]?|[0-9]*[.][0-9]+)(?:e[-+]?[0-9]+)?d", Pattern.CASE_INSENSITIVE); private static final MojangsonParser MOJANGSON_PARSER = new MojangsonParser(new StringReader("")); - public static Object serialize(NBTBase base) { - if (base instanceof NBTTagCompound) { - Map innerMap = new HashMap<>(); - for (String key : ((NBTTagCompound) base).getAllKeys()) { - innerMap.put(key, serialize(((NBTTagCompound) base).get(key))); - } - - return innerMap; - } else if (base instanceof NBTTagList) { - List baseList = new ArrayList<>(); - for (int i = 0; i < ((NBTList) base).size(); i++) { - baseList.add(serialize((NBTBase) ((NBTList) base).get(i))); - } - - return baseList; - } else if (base instanceof NBTTagString) { - return base.getAsString(); - } else if (base instanceof NBTTagInt) { // No need to check for doubles, those are covered by the double itself - return base.toString() + "i"; - } - - return base.toString(); + public static String serialize(@NotNull final NBTBase base) { + final SnbtPrinterTagVisitor snbtVisitor = new SnbtPrinterTagVisitor(); + return snbtVisitor.visit(base); } - public static NBTBase deserialize(Object object) { + public static NBTBase deserialize(final Object object) { + // The new logic expects the top level object to be a single string, holding the entire nbt tag as SNBT. + if (object instanceof final String snbtString) { + try { + return MojangsonParser.parseTag(snbtString); + } catch (final CommandSyntaxException e) { + throw new RuntimeException("Failed to deserialise nbt", e); + } + } else { // Legacy logic is passed to the internal legacy deserialization that attempts to read the old format that *unsuccessfully* attempted to read/write nbt to a full yml tree. + return internalLegacyDeserialization(object); + } + } + + private static NBTBase internalLegacyDeserialization(@NotNull final Object object) { if (object instanceof Map) { NBTTagCompound compound = new NBTTagCompound(); for (Map.Entry entry : ((Map) object).entrySet()) { - compound.put(entry.getKey(), deserialize(entry.getValue())); + compound.put(entry.getKey(), internalLegacyDeserialization(entry.getValue())); } return compound; @@ -63,7 +59,7 @@ public class CraftNBTTagConfigSerializer { NBTTagList tagList = new NBTTagList(); for (Object tag : list) { - tagList.add(deserialize(tag)); + tagList.add(internalLegacyDeserialization(tag)); } return tagList; diff --git a/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java b/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java index ef566f095..b45afe700 100644 --- a/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java +++ b/src/test/java/org/bukkit/craftbukkit/inventory/PersistentDataContainerTest.java @@ -1,6 +1,7 @@ package org.bukkit.craftbukkit.inventory; import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; import java.io.StringReader; import java.lang.reflect.Array; import java.nio.ByteBuffer; @@ -10,6 +11,7 @@ import net.minecraft.nbt.NBTTagCompound; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -164,6 +166,49 @@ public class PersistentDataContainerTest extends AbstractTestingBase { return itemMeta; } + /* + Test edge cases with strings + */ + @Test + public void testStringEdgeCases() throws IOException, InvalidConfigurationException { + final ItemStack stack = new ItemStack(Material.DIAMOND); + final ItemMeta meta = stack.getItemMeta(); + assertNotNull(meta); + + final String arrayLookalike = "[\"UnicornParticle\",\"TotemParticle\",\"AngelParticle\",\"ColorSwitchParticle\"]"; + final String jsonLookalike = """ + { + "key": 'A value wrapped in single quotes', + "other": "A value with normal quotes", + "array": ["working", "unit", "tests"] + } + """; + + final PersistentDataContainer pdc = meta.getPersistentDataContainer(); + pdc.set(requestKey("string_int"), PersistentDataType.STRING, "5i"); + pdc.set(requestKey("string_true"), PersistentDataType.STRING, "true"); + pdc.set(requestKey("string_byte_array"), PersistentDataType.STRING, "[B;-128B]"); + pdc.set(requestKey("string_array_lookalike"), PersistentDataType.STRING, arrayLookalike); + pdc.set(requestKey("string_json_lookalike"), PersistentDataType.STRING, jsonLookalike); + + stack.setItemMeta(meta); + + final YamlConfiguration config = new YamlConfiguration(); + config.set("test", stack); + config.load(new StringReader(config.saveToString())); // Reload config from string + + final ItemStack loadedStack = config.getItemStack("test"); + assertNotNull(loadedStack); + final ItemMeta loadedMeta = loadedStack.getItemMeta(); + assertNotNull(loadedMeta); + + final PersistentDataContainer loadedPdc = loadedMeta.getPersistentDataContainer(); + assertEquals("5i", loadedPdc.get(requestKey("string_int"), PersistentDataType.STRING)); + assertEquals("true", loadedPdc.get(requestKey("string_true"), PersistentDataType.STRING)); + assertEquals(arrayLookalike, loadedPdc.get(requestKey("string_array_lookalike"), PersistentDataType.STRING)); + assertEquals(jsonLookalike, loadedPdc.get(requestKey("string_json_lookalike"), PersistentDataType.STRING)); + } + /* Test complex object storage */ diff --git a/src/test/java/org/bukkit/craftbukkit/legacy/PersistentDataContainerLegacyTest.java b/src/test/java/org/bukkit/craftbukkit/legacy/PersistentDataContainerLegacyTest.java new file mode 100644 index 000000000..21f11bb8c --- /dev/null +++ b/src/test/java/org/bukkit/craftbukkit/legacy/PersistentDataContainerLegacyTest.java @@ -0,0 +1,66 @@ +package org.bukkit.craftbukkit.legacy; + +import static org.junit.jupiter.api.Assertions.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.bukkit.NamespacedKey; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.craftbukkit.inventory.CraftItemFactory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.support.AbstractTestingBase; +import org.junit.jupiter.api.Test; + +public class PersistentDataContainerLegacyTest extends AbstractTestingBase { + + @Test + public void ensureLegacyParsing() { + CraftItemFactory.instance(); // Initialize craft item factory to register craft item meta serializers + + YamlConfiguration legacyConfig = null; + try (final InputStream input = getClass().getClassLoader().getResourceAsStream("pdc/legacy_pdc.yml")) { + assertNotNull(input, "Legacy pdc yaml was null"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) { + legacyConfig = YamlConfiguration.loadConfiguration(reader); + } + } catch (IOException e) { + fail("Failed to find test resource!"); + } + + assertNotNull(legacyConfig, "Could not fetch legacy config"); + + final ItemStack stack = legacyConfig.getItemStack("test"); + assertNotNull(stack); + + final ItemMeta meta = stack.getItemMeta(); + assertNotNull(meta); + + final PersistentDataContainer pdc = meta.getPersistentDataContainer(); + assertEquals(Byte.valueOf(Byte.MAX_VALUE), pdc.get(key("byte"), PersistentDataType.BYTE), "legacy byte was wrong"); + assertEquals(Short.valueOf(Short.MAX_VALUE), pdc.get(key("short"), PersistentDataType.SHORT), "legacy short was wrong"); + assertEquals(Integer.valueOf(Integer.MAX_VALUE), pdc.get(key("integer"), PersistentDataType.INTEGER), "legacy integer was wrong"); + assertEquals(Long.valueOf(Long.MAX_VALUE), pdc.get(key("long"), PersistentDataType.LONG), "legacy long was wrong"); + assertEquals(Float.valueOf(Float.MAX_VALUE), pdc.get(key("float"), PersistentDataType.FLOAT), "legacy float was wrong"); + assertEquals(Double.valueOf(Double.MAX_VALUE), pdc.get(key("double"), PersistentDataType.DOUBLE), "legacy double was wrong"); + assertEquals("stringy", pdc.get(key("string_simple"), PersistentDataType.STRING), "legacy string-simple was wrong"); + assertEquals("What a fun complex string 🔥", pdc.get(key("string_complex"), PersistentDataType.STRING), "legacy string-complex was wrong"); + + assertArrayEquals(new byte[]{Byte.MIN_VALUE}, pdc.get(key("byte_array"), PersistentDataType.BYTE_ARRAY), "legacy byte array was wrong"); + + assertArrayEquals(new int[]{Integer.MIN_VALUE}, pdc.get(key("integer_array"), PersistentDataType.INTEGER_ARRAY), "legacy integer array was wrong"); + + assertArrayEquals(new long[]{Long.MIN_VALUE}, pdc.get(key("long_array"), PersistentDataType.LONG_ARRAY), "legacy long array was wrong"); + + assertEquals("5", pdc.get(key("string_edge_case_number"), PersistentDataType.STRING), "legacy string edge case number"); + assertEquals("\"Hello world\"", pdc.get(key("string_edge_case_quoted"), PersistentDataType.STRING), "legacy string edge case quotes"); + } + + private NamespacedKey key(String key) { + return new NamespacedKey("test", key); + } +} diff --git a/src/test/resources/pdc/legacy_pdc.yml b/src/test/resources/pdc/legacy_pdc.yml new file mode 100644 index 000000000..6cea2fe7a --- /dev/null +++ b/src/test/resources/pdc/legacy_pdc.yml @@ -0,0 +1,23 @@ +test: + ==: org.bukkit.inventory.ItemStack + v: 2584 + type: NETHER_STAR + meta: + ==: ItemMeta + meta-type: UNSPECIFIC + PublicBukkitValues: + test:string_simple: stringy + test:integer: 2147483647i + test:long_array: '[L;-9223372036854775808L]' + test:byte: 127b + test:double: 1.7976931348623157E308d + test:short: 32767s + test:string_complex: What a fun complex string 🔥 + test:integer_array: '[I;-2147483648]' + test:float: 3.4028235E38f + test:byte_array: '[B;-128B]' + test:long: 9223372036854775807L + test:string_edge_case_number: '5' + # Constructed via set(key, STRING, "\"Hello world\"") in legacy + test:string_edge_case_quoted: '"Hello world"' +