diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java
index 58166c75fd9..a517b701d47 100644
--- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java
+++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java
@@ -24,6 +24,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -528,53 +529,73 @@ static byte[] decode(String hex) {
return decoded;
}
- /**
- * A regex pattern of a line in a CSV file.
- *
- * - {@code (?<=^|,)}: assert that the match is preceded by the start of the line or a comma
- * - {@code (?:([^",]*)|"((?:[^"]+|"")*)")}: match either a quoted or unquoted value
- *
- * - - {@code ([^",]*)}: match an unquoted value
- * - - {@code "((?:[^"]+|"")*)"}: match a quoted value
- *
- * - {@code (?:,|$)}: match either a comma or the end of the line
- *
- */
- private static final Pattern CSV_LINE_PATTERN = Pattern.compile("(?<=^|,)\\s*(?:([^\",]*)|\"((?:[^\"]+|\"\")*)\")\\s*(?:,|$)");
-
/**
* Splits the given CSV line into its values.
+ * Supports quoted fields (which may contain commas) and doubled-quote
+ * escaping ("" -> ") inside quoted fields. Unquoted values are trimmed;
+ * quoted values are taken verbatim (aside from unescaping).
*
* @param line the CSV line.
- * @return the array of values.
- *
- * @see #CSV_LINE_PATTERN
+ * @return the array of values, or null if the line is malformed.
*/
@Nullable
static String[] splitCSV(String line) {
- Matcher matcher = CSV_LINE_PATTERN.matcher(line);
-
- int lastEnd = 0;
- ArrayList result = new ArrayList<>();
+ int n = line.length();
+ int pos = 0;
+ List result = new ArrayList<>();
+
+ while (true) {
+ // skip leading whitespace before a value
+ while (pos < n && Character.isWhitespace(line.charAt(pos)))
+ pos++;
+
+ if (pos < n && line.charAt(pos) == '"') {
+ // quoted value
+ pos++; // consume opening quote
+ StringBuilder value = new StringBuilder();
+ boolean closed = false;
+ while (pos < n) {
+ char c = line.charAt(pos);
+ if (c == '"') {
+ if (pos + 1 < n && line.charAt(pos + 1) == '"') {
+ value.append('"'); // escaped quote
+ pos += 2;
+ } else {
+ pos++; // consume closing quote
+ closed = true;
+ break;
+ }
+ } else {
+ value.append(c);
+ pos++;
+ }
+ }
+ if (!closed)
+ return null; // unterminated quoted value
- while (matcher.find()) {
- if (lastEnd != matcher.start())
- return null; // other stuff in between finds
+ // skip trailing whitespace after the closing quote
+ while (pos < n && Character.isWhitespace(line.charAt(pos)))
+ pos++;
- if (matcher.group(1) != null) {
- // unquoted, leave as is
- result.add(matcher.group(1).trim());
+ result.add(value.toString());
} else {
- // quoted, remove quotes
- result.add(matcher.group(2).replace("\"\"", "\""));
+ // unquoted value: read until comma, rejecting stray quotes
+ int start = pos;
+ while (pos < n && line.charAt(pos) != ',') {
+ if (line.charAt(pos) == '"')
+ return null; // quote in the middle of an unquoted value
+ pos++;
+ }
+ result.add(line.substring(start, pos).trim());
}
- lastEnd = matcher.end();
+ if (pos >= n)
+ break; // end of line, done
+ if (line.charAt(pos) != ',')
+ return null; // expected a comma here, found something else
+ pos++; // consume the comma and parse the next value
}
- if (lastEnd != line.length())
- return null; // other stuff after last find
-
return result.toArray(new String[0]);
}
diff --git a/src/main/java/ch/njol/skript/variables/VariablesMap.java b/src/main/java/ch/njol/skript/variables/VariablesMap.java
index a3dc356f16d..dfdbd42d34d 100644
--- a/src/main/java/ch/njol/skript/variables/VariablesMap.java
+++ b/src/main/java/ch/njol/skript/variables/VariablesMap.java
@@ -257,7 +257,7 @@ void setVariable(String name, @Nullable Object value) {
parent.put(childNodeName, childNode);
parent = (TreeMap) childNode;
} else {
- // Want to set variable to null, bu variable is already null
+ // Want to set variable to null, but variable is already null
break;
}
} else if (childNode instanceof TreeMap) {
@@ -297,11 +297,7 @@ void setVariable(String name, @Nullable Object value) {
// Ran into leaf node
if (i == split.length - 1) {
// If we arrived at the end of the variable name, update parent
- if (value == null)
- parent.remove(childNodeName);
- else
- parent.put(childNodeName, value);
-
+ parent.compute(childNodeName, (k, v) -> value);
break;
} else if (value != null) {
// Need to continue iteration, create new child node and put old value in it
diff --git a/src/main/java/ch/njol/yggdrasil/DefaultYggdrasilInputStream.java b/src/main/java/ch/njol/yggdrasil/DefaultYggdrasilInputStream.java
index 6926cb3ce8e..cf52fdc6abf 100644
--- a/src/main/java/ch/njol/yggdrasil/DefaultYggdrasilInputStream.java
+++ b/src/main/java/ch/njol/yggdrasil/DefaultYggdrasilInputStream.java
@@ -17,7 +17,7 @@
// x(): read info & data (e.g. content type, contents) [i.e. no tag]
// _x(): read data only (e.g. contents)
public final class DefaultYggdrasilInputStream extends YggdrasilInputStream {
-
+
private final InputStream in;
private final short version;
@@ -32,12 +32,12 @@ public DefaultYggdrasilInputStream(Yggdrasil yggdrasil, InputStream in) throws I
}
/**
- * @throws EOFException If the end of the stream is reached
+ * @throws FastEOFException If the end of the stream is reached
*/
private int read() throws IOException {
int b = in.read();
if (b < 0)
- throw new EOFException();
+ throw new FastEOFException();
return b;
}
@@ -51,7 +51,7 @@ private void readFully(byte[] buf, int startLength) throws IOException {
while (length > 0) {
int n = in.read(buf, offset, length);
if (n < 0)
- throw new EOFException("Expected " + startLength + " bytes, but could only read " + (startLength - length));
+ throw new FastEOFException("Expected " + startLength + " bytes, but could only read " + (startLength - length));
offset += n;
length -= n;
}
@@ -65,7 +65,7 @@ private String readShortString() throws IOException {
int i = version <= 1 ? readInt() : readUnsignedInt();
if (i < 0 || i > readShortStrings.size())
throw new StreamCorruptedException("Invalid short string reference " + i);
- return "" + readShortStrings.get(i);
+ return readShortStrings.get(i);
}
byte[] d = new byte[length];
readFully(d);
@@ -112,12 +112,12 @@ private int readUnsignedInt() throws IOException {
return (b & ~0x80) << 8 | read();
return b << 24 | read() << 16 | read() << 8 | read();
}
-
+
private long readLong() throws IOException {
- return (long) in.read() << 56 | (long) in.read() << 48
- | (long) in.read() << 40 | (long) in.read() << 32
- | (long) in.read() << 24 | (long) in.read() << 16
- | (long) in.read() << 8 | read();
+ return (long) read() << 56 | (long) read() << 48
+ | (long) read() << 40 | (long) read() << 32
+ | (long) read() << 24 | (long) read() << 16
+ | (long) read() << 8 | read();
}
private float readFloat() throws IOException {
@@ -143,26 +143,17 @@ else if (r == 1)
@Override
protected Object readPrimitive(Tag type) throws IOException {
- switch (type) {
- case T_BYTE:
- return readByte();
- case T_SHORT:
- return readShort();
- case T_INT:
- return readInt();
- case T_LONG:
- return readLong();
- case T_FLOAT:
- return readFloat();
- case T_DOUBLE:
- return readDouble();
- case T_CHAR:
- return readChar();
- case T_BOOLEAN:
- return readBoolean();
- default:
- throw new YggdrasilException("Internal error; " + type);
- }
+ return switch (type) {
+ case T_BYTE -> readByte();
+ case T_SHORT -> readShort();
+ case T_INT -> readInt();
+ case T_LONG -> readLong();
+ case T_FLOAT -> readFloat();
+ case T_DOUBLE -> readDouble();
+ case T_CHAR -> readChar();
+ case T_BOOLEAN -> readBoolean();
+ default -> throw new YggdrasilException("Internal error; " + type);
+ };
}
@Override
@@ -205,40 +196,16 @@ protected Class> readClass() throws IOException {
int dimensions = 0;
while ((type = readTag()) == T_ARRAY)
dimensions++;
- Class> clazz;
- switch (type) {
- case T_OBJECT:
- case T_ENUM:
- clazz = yggdrasil.getClass(readShortString());
- break;
- case T_BOOLEAN:
- case T_BOOLEAN_OBJ:
- case T_BYTE:
- case T_BYTE_OBJ:
- case T_CHAR:
- case T_CHAR_OBJ:
- case T_DOUBLE:
- case T_DOUBLE_OBJ:
- case T_FLOAT:
- case T_FLOAT_OBJ:
- case T_INT:
- case T_INT_OBJ:
- case T_LONG:
- case T_LONG_OBJ:
- case T_SHORT:
- case T_SHORT_OBJ:
- case T_CLASS:
- case T_STRING:
+ Class> clazz = switch (type) {
+ case T_OBJECT, T_ENUM -> yggdrasil.getClass(readShortString());
+ case T_BOOLEAN, T_BOOLEAN_OBJ, T_BYTE, T_BYTE_OBJ, T_CHAR, T_CHAR_OBJ, T_DOUBLE, T_DOUBLE_OBJ, T_FLOAT,
+ T_FLOAT_OBJ, T_INT, T_INT_OBJ, T_LONG, T_LONG_OBJ, T_SHORT, T_SHORT_OBJ, T_CLASS, T_STRING -> {
assert type.type != null;
- clazz = type.type;
- break;
- case T_NULL:
- case T_REFERENCE:
- throw new StreamCorruptedException("unexpected tag " + type);
- case T_ARRAY:
- default:
- throw new YggdrasilException("Internal error; " + type);
- }
+ yield type.type;
+ }
+ case T_NULL, T_REFERENCE -> throw new StreamCorruptedException("unexpected tag " + type);
+ default -> throw new YggdrasilException("Internal error; " + type);
+ };
while (dimensions-- > 0)
clazz = CollectionUtils.arrayType(clazz);
return clazz;
diff --git a/src/main/java/ch/njol/yggdrasil/FastEOFException.java b/src/main/java/ch/njol/yggdrasil/FastEOFException.java
new file mode 100644
index 00000000000..ebdfb698b8c
--- /dev/null
+++ b/src/main/java/ch/njol/yggdrasil/FastEOFException.java
@@ -0,0 +1,20 @@
+package ch.njol.yggdrasil;
+
+import java.io.EOFException;
+
+public class FastEOFException extends EOFException {
+
+ public FastEOFException() {
+ super();
+ }
+
+ public FastEOFException(String message) {
+ super(message);
+ }
+
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return this;
+ }
+
+}
diff --git a/src/main/java/ch/njol/yggdrasil/Fields.java b/src/main/java/ch/njol/yggdrasil/Fields.java
index 1d3d332eaa7..06d12900d70 100644
--- a/src/main/java/ch/njol/yggdrasil/Fields.java
+++ b/src/main/java/ch/njol/yggdrasil/Fields.java
@@ -53,7 +53,6 @@ public Class> getType() {
if (value == null)
return null;
Class> type = value.getClass();
- assert type != null;
return isPrimitiveValue ? Tag.getPrimitiveFromWrapper(type).type : type;
}
@@ -134,9 +133,8 @@ public boolean equals(@Nullable Object object) {
return true;
if (object == null)
return false;
- if (!(object instanceof FieldContext))
+ if (!(object instanceof FieldContext other))
return false;
- FieldContext other = (FieldContext) object;
return id.equals(other.id);
}
@@ -198,7 +196,6 @@ public Fields(Object object) throws NotSerializableException {
public Fields(Object object, @Nullable Yggdrasil yggdrasil) throws NotSerializableException {
this.yggdrasil = yggdrasil;
Class> type = object.getClass();
- assert type != null;
for (Field field : getFields(type)) {
assert field != null;
try {
@@ -312,7 +309,6 @@ public void setFields(Object object) throws StreamCorruptedException, NotSeriali
throw new YggdrasilException("");
Set excessive = new HashSet<>(fields.values());
Class> type = object.getClass();
- assert type != null;
for (Field field : getFields(type)) {
assert field != null;
String id = Yggdrasil.getID(field);
diff --git a/src/main/java/ch/njol/yggdrasil/JRESerializer.java b/src/main/java/ch/njol/yggdrasil/JRESerializer.java
index 2f6ae9eefe2..e5cc6c5d0d1 100644
--- a/src/main/java/ch/njol/yggdrasil/JRESerializer.java
+++ b/src/main/java/ch/njol/yggdrasil/JRESerializer.java
@@ -42,16 +42,18 @@ public Fields serialize(Object object) {
if (!SUPPORTED_CLASSES.contains(object.getClass()))
throw new IllegalArgumentException();
Fields fields = new Fields();
- if (object instanceof Collection) {
- Collection> collection = ((Collection>) object);
- fields.putObject("values", collection.toArray());
- } else if (object instanceof Map) {
- Map, ?> map = ((Map, ?>) object);
- fields.putObject("keys", map.keySet().toArray());
- fields.putObject("values", map.values().toArray());
- } else if (object instanceof UUID) {
- fields.putPrimitive("mostSigBits", ((UUID) object).getMostSignificantBits());
- fields.putPrimitive("leastSigBits", ((UUID) object).getLeastSignificantBits());
+ switch (object) {
+ case Collection> collection -> fields.putObject("values", collection.toArray());
+ case Map, ?> map -> {
+ fields.putObject("keys", map.keySet().toArray());
+ fields.putObject("values", map.values().toArray());
+ }
+ case UUID uuid -> {
+ fields.putPrimitive("mostSigBits", uuid.getMostSignificantBits());
+ fields.putPrimitive("leastSigBits", uuid.getLeastSignificantBits());
+ }
+ default -> {
+ }
}
assert fields.size() > 0 : object;
return fields;
@@ -83,15 +85,13 @@ public T newInstance(Class type) {
@Override
public void deserialize(Object object, Fields fields) throws StreamCorruptedException {
try {
- if (object instanceof Collection) {
- Collection> collection = ((Collection>) object);
+ if (object instanceof Collection> collection) {
Object[] values = fields.getObject("values", Object[].class);
if (values == null)
throw new StreamCorruptedException();
collection.addAll((Collection) Arrays.asList(values));
return;
- } else if (object instanceof Map) {
- Map, ?> map = ((Map, ?>) object);
+ } else if (object instanceof Map, ?> map) {
Object[] keys = fields.getObject("keys", Object[].class), values = fields.getObject("values", Object[].class);
if (keys == null || values == null || keys.length != values.length)
throw new StreamCorruptedException();
diff --git a/src/main/java/ch/njol/yggdrasil/YggdrasilException.java b/src/main/java/ch/njol/yggdrasil/YggdrasilException.java
index 8263fa4a28a..51072c3b832 100644
--- a/src/main/java/ch/njol/yggdrasil/YggdrasilException.java
+++ b/src/main/java/ch/njol/yggdrasil/YggdrasilException.java
@@ -1,5 +1,7 @@
package ch.njol.yggdrasil;
+import java.io.Serial;
+
/**
* Thrown if the object(s) that should be saved/loaded with Yggdrasil do
* not comply with its requirements, or if Yggdrasil is used incorrectly.
@@ -8,6 +10,7 @@
*/
public final class YggdrasilException extends RuntimeException {
+ @Serial
private static final long serialVersionUID = -6130660396780458226L;
public YggdrasilException(String message) {
diff --git a/src/main/java/ch/njol/yggdrasil/YggdrasilInputStream.java b/src/main/java/ch/njol/yggdrasil/YggdrasilInputStream.java
index 2aa9f97ac8a..b1bd5e28dea 100644
--- a/src/main/java/ch/njol/yggdrasil/YggdrasilInputStream.java
+++ b/src/main/java/ch/njol/yggdrasil/YggdrasilInputStream.java
@@ -127,23 +127,17 @@ private Object readObject(Tag tag) throws IOException {
}
Object object;
switch (tag) {
- case T_ARRAY: {
+ case T_ARRAY -> {
Class> type = readArrayComponentType();
object = Array.newInstance(type, readArrayLength());
readObjects.add(object);
readArrayContents(object);
return object;
}
- case T_CLASS:
- object = readClass();
- break;
- case T_ENUM:
- object = readEnum();
- break;
- case T_STRING:
- object = readString();
- break;
- case T_OBJECT: {
+ case T_CLASS -> object = readClass();
+ case T_ENUM -> object = readEnum();
+ case T_STRING -> object = readString();
+ case T_OBJECT -> {
Class> c = readObjectType();
YggdrasilSerializer s = yggdrasil.getSerializer(c);
if (s != null && !s.canBeInstantiated(c)) {
@@ -170,30 +164,18 @@ private Object readObject(Tag tag) throws IOException {
}
return object;
}
- case T_BOOLEAN_OBJ:
- case T_BYTE_OBJ:
- case T_CHAR_OBJ:
- case T_DOUBLE_OBJ:
- case T_FLOAT_OBJ:
- case T_INT_OBJ:
- case T_LONG_OBJ:
- case T_SHORT_OBJ:
+ case T_BOOLEAN_OBJ, T_BYTE_OBJ, T_CHAR_OBJ, T_DOUBLE_OBJ, T_FLOAT_OBJ, T_INT_OBJ, T_LONG_OBJ,
+ T_SHORT_OBJ -> {
Tag primitive = tag.getPrimitive();
assert primitive != null;
object = readPrimitive(primitive);
- break;
- case T_BYTE:
- case T_BOOLEAN:
- case T_CHAR:
- case T_DOUBLE:
- case T_FLOAT:
- case T_INT:
- case T_LONG:
- case T_SHORT:
+ }
+ case T_BYTE, T_BOOLEAN, T_CHAR, T_DOUBLE, T_FLOAT, T_INT, T_LONG, T_SHORT ->
throw new StreamCorruptedException();
- default:
+ default -> {
assert false;
throw new StreamCorruptedException();
+ }
}
readObjects.add(object);
return object;
diff --git a/src/main/java/ch/njol/yggdrasil/YggdrasilOutputStream.java b/src/main/java/ch/njol/yggdrasil/YggdrasilOutputStream.java
index 539ee9bc649..b055c43ec22 100644
--- a/src/main/java/ch/njol/yggdrasil/YggdrasilOutputStream.java
+++ b/src/main/java/ch/njol/yggdrasil/YggdrasilOutputStream.java
@@ -131,7 +131,6 @@ protected final void writeReference(int ref) throws IOException {
@SuppressWarnings({"rawtypes", "unchecked"})
private void writeGenericObject(Object object, int ref) throws IOException {
Class> type = object.getClass();
- assert type != null;
if (!yggdrasil.isSerializable(type))
throw new NotSerializableException(type.getName());
Fields fields;
@@ -190,26 +189,17 @@ public final void writeObject(@Nullable Object object) throws IOException {
return;
}
switch (type) {
- case T_ARRAY:
- writeArray(object);
- return;
- case T_STRING:
- writeString((String) object);
- return;
- case T_ENUM:
+ case T_ARRAY -> writeArray(object);
+ case T_STRING -> writeString((String) object);
+ case T_ENUM -> {
if (object instanceof Enum)
writeEnum((Enum>) object);
else
writeEnum((PseudoEnum>) object);
- return;
- case T_CLASS:
- writeClass((Class>) object);
- return;
- case T_OBJECT:
- writeGenericObject(object, ref);
- return;
- default:
- throw new YggdrasilException("unhandled type " + type);
+ }
+ case T_CLASS -> writeClass((Class>) object);
+ case T_OBJECT -> writeGenericObject(object, ref);
+ default -> throw new YggdrasilException("unhandled type " + type);
}
}
diff --git a/src/main/java/ch/njol/yggdrasil/YggdrasilSerializer.java b/src/main/java/ch/njol/yggdrasil/YggdrasilSerializer.java
index 08a90ff67f4..ea962518dc3 100644
--- a/src/main/java/ch/njol/yggdrasil/YggdrasilSerializer.java
+++ b/src/main/java/ch/njol/yggdrasil/YggdrasilSerializer.java
@@ -16,12 +16,12 @@ public abstract class YggdrasilSerializer implements ClassResolver {
public abstract Class extends T> getClass(String id);
/**
- * Serialises the given object.
+ * Serializes the given object.
*
- * Use return new {@link Fields#Fields(Object) Fields}(this); to emulate the default behaviour.
+ * Use return new {@link Fields#Fields(Object) Fields}(this); to emulate the default behavior.
*
- * @param object The object to serialise
- * @return A Fields object representing the object's fields to serialise. Must not be null.
+ * @param object The object to serialize
+ * @return A Fields object representing the object's fields to serialize. Must not be null.
* @throws NotSerializableException If this object could not be serialized
*/
public abstract Fields serialize(T object) throws NotSerializableException;
@@ -54,7 +54,7 @@ public boolean canBeInstantiated(Class extends T> type) {
/**
* Deserializes an object.
*
- * Use fields.{@link Fields#setFields(Object) setFields}(o); to emulate the default behaviour.
+ * Use fields.{@link Fields#setFields(Object) setFields}(o); to emulate the default behavior.
*
* @param object The object to deserialize as returned by {@link #newInstance(Class)}.
* @param fields The fields read from stream