SPIGOT-7804: Fix written book serialization

* Account for null/missing values when deserializing the 'resolved' and
  'generation' fields.
* Serialize the book pages as JSON strings.
* Avoid redundant conversion from strings to components to JSON and back to
  components during deserialization.
  Add CraftChatMessage.fromJSONOrString that accepts a maxLength argument
  and remove the no longer used fromJSONOrStringToJSON, fromStringToJSON,
  and fromJSONComponent helper methods.
This commit is contained in:
blablubbabc 2024-06-30 16:27:23 +10:00 committed by md_5
parent aac911d263
commit 62e3b73a49
No known key found for this signature in database
GPG Key ID: E8E901AC7C617C11
3 changed files with 19 additions and 58 deletions

View File

@ -3,6 +3,7 @@ package org.bukkit.craftbukkit.inventory;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Lists;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -35,8 +36,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta {
protected String title; protected String title;
protected String author; protected String author;
// We store the pages in their raw original text representation. See SPIGOT-5063, SPIGOT-5350, SPIGOT-3206 // Stored as components and serialized as JSON. See SPIGOT-5063, SPIGOT-5350, SPIGOT-3206
// For written books (CraftMetaBookSigned) the pages are stored in Minecraft's JSON format.
protected List<IChatBaseComponent> pages; // null and empty are two different states internally protected List<IChatBaseComponent> pages; // null and empty are two different states internally
protected boolean resolved; protected boolean resolved;
protected int generation; protected int generation;
@ -105,13 +105,13 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta {
this.pages = new ArrayList<IChatBaseComponent>(); this.pages = new ArrayList<IChatBaseComponent>();
for (Object page : pages) { for (Object page : pages) {
if (page instanceof String) { if (page instanceof String) {
internalAddPage(CraftChatMessage.fromJSON(CraftChatMessage.fromJSONOrStringToJSON((String) page, false, true, MAX_PAGE_LENGTH, false))); internalAddPage(CraftChatMessage.fromJSONOrString((String) page, false, true, MAX_PAGE_LENGTH, false));
} }
} }
} }
resolved = SerializableMeta.getObject(Boolean.class, map, RESOLVED.BUKKIT, true); resolved = SerializableMeta.getBoolean(map, RESOLVED.BUKKIT);
generation = SerializableMeta.getObject(Integer.class, map, GENERATION.BUKKIT, true); generation = SerializableMeta.getInteger(map, GENERATION.BUKKIT);
} }
@Override @Override
@ -351,7 +351,7 @@ public class CraftMetaBookSigned extends CraftMetaItem implements BookMeta {
} }
if (pages != null) { if (pages != null) {
builder.put(BOOK_PAGES.BUKKIT, ImmutableList.copyOf(pages)); builder.put(BOOK_PAGES.BUKKIT, ImmutableList.copyOf(Lists.transform(pages, CraftChatMessage::toJSON)));
} }
if (resolved) { if (resolved) {

View File

@ -96,6 +96,11 @@ public final class SerializableMeta implements ConfigurationSerializable {
return value != null && value; return value != null && value;
} }
public static int getInteger(Map<?, ?> map, Object field) {
Integer value = getObject(Integer.class, map, field, true);
return value != null ? value : 0;
}
public static <T> T getObject(Class<T> clazz, Map<?, ?> map, Object field, boolean nullable) { public static <T> T getObject(Class<T> clazz, Map<?, ?> map, Object field, boolean nullable) {
final Object object = map.get(field); final Object object = map.get(field);

View File

@ -231,51 +231,26 @@ public final class CraftChatMessage {
} }
public static IChatBaseComponent fromJSONOrString(String message, boolean nullable, boolean keepNewlines) { public static IChatBaseComponent fromJSONOrString(String message, boolean nullable, boolean keepNewlines) {
return fromJSONOrString(message, nullable, keepNewlines, Integer.MAX_VALUE, false);
}
public static IChatBaseComponent fromJSONOrString(String message, boolean nullable, boolean keepNewlines, int maxLength, boolean checkJsonContentLength) {
if (message == null) message = ""; if (message == null) message = "";
if (nullable && message.isEmpty()) return null; if (nullable && message.isEmpty()) return null;
IChatBaseComponent component = fromJSONOrNull(message); IChatBaseComponent component = fromJSONOrNull(message);
if (component != null) {
return component;
} else {
return fromString(message, keepNewlines)[0];
}
}
public static String fromJSONOrStringToJSON(String message) {
return fromJSONOrStringToJSON(message, false);
}
public static String fromJSONOrStringToJSON(String message, boolean keepNewlines) {
return fromJSONOrStringToJSON(message, false, keepNewlines, Integer.MAX_VALUE, false);
}
public static String fromJSONOrStringOrNullToJSON(String message) {
return fromJSONOrStringOrNullToJSON(message, false);
}
public static String fromJSONOrStringOrNullToJSON(String message, boolean keepNewlines) {
return fromJSONOrStringToJSON(message, true, keepNewlines, Integer.MAX_VALUE, false);
}
public static String fromJSONOrStringToJSON(String message, boolean nullable, boolean keepNewlines, int maxLength, boolean checkJsonContentLength) {
if (message == null) message = "";
if (nullable && message.isEmpty()) return null;
// If the input can be parsed as JSON, we use that:
IChatBaseComponent component = fromJSONOrNull(message);
if (component != null) { if (component != null) {
if (checkJsonContentLength) { if (checkJsonContentLength) {
String content = fromComponent(component); String content = fromComponent(component);
String trimmedContent = trimMessage(content, maxLength); String trimmedContent = trimMessage(content, maxLength);
if (content != trimmedContent) { // identity comparison is fine here if (content != trimmedContent) { // Identity comparison is fine here
// Note: The resulting text has all non-plain text features stripped. // Note: The resulting text has all non-plain text features stripped.
return fromStringToJSON(trimmedContent, keepNewlines); return fromString(trimmedContent, keepNewlines)[0];
} }
} }
return message; return component;
} else { } else {
// Else we interpret the input as legacy text:
message = trimMessage(message, maxLength); message = trimMessage(message, maxLength);
return fromStringToJSON(message, keepNewlines); return fromString(message, keepNewlines)[0];
} }
} }
@ -287,25 +262,6 @@ public final class CraftChatMessage {
} }
} }
public static String fromStringToJSON(String message) {
return fromStringToJSON(message, false);
}
public static String fromStringToJSON(String message, boolean keepNewlines) {
IChatBaseComponent component = CraftChatMessage.fromString(message, keepNewlines)[0];
return CraftChatMessage.toJSON(component);
}
public static String fromStringOrNullToJSON(String message) {
IChatBaseComponent component = CraftChatMessage.fromStringOrNull(message);
return CraftChatMessage.toJSONOrNull(component);
}
public static String fromJSONComponent(String jsonMessage) {
IChatBaseComponent component = CraftChatMessage.fromJSONOrNull(jsonMessage);
return CraftChatMessage.fromComponent(component);
}
public static String fromComponent(IChatBaseComponent component) { public static String fromComponent(IChatBaseComponent component) {
if (component == null) return ""; if (component == null) return "";
StringBuilder out = new StringBuilder(); StringBuilder out = new StringBuilder();