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. - * - */ - 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 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 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