Update adventure nbt i/o

This commit is contained in:
Nassim Jahnke 2021-09-25 13:15:26 +02:00
parent 12e2be40e8
commit 88165088a9
No known key found for this signature in database
GPG Key ID: 6BE3B555EBC5982B
6 changed files with 168 additions and 92 deletions

View File

@ -39,8 +39,12 @@ import java.nio.file.Path;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
// Specific Via changes:
// - Use OpenNBT tags
// - Added readString/writeString methods from TagStringIO
// - Has not been updated for the sake of keeping the class simple
/**
* See https://github.com/KyoriPowered/adventure.
* Serialization operations for binary tags.
*/
public final class BinaryTagIO {
private BinaryTagIO() {

View File

@ -1,7 +1,7 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2020 KyoriPowered
* Copyright (c) 2017-2021 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -23,7 +23,10 @@
*/
package com.viaversion.viaversion.api.minecraft.nbt;
/* package */ final class CharBuffer {
/**
* A character buffer designed to be inspected by a parser.
*/
final class CharBuffer {
private final CharSequence sequence;
private int index;
@ -32,7 +35,7 @@ package com.viaversion.viaversion.api.minecraft.nbt;
}
/**
* Get the character at the current position
* Get the character at the current position.
*
* @return The current character
*/
@ -45,7 +48,7 @@ package com.viaversion.viaversion.api.minecraft.nbt;
}
/**
* Get the current character and advance
* Get the current character and advance.
*
* @return current character
*/
@ -62,6 +65,10 @@ package com.viaversion.viaversion.api.minecraft.nbt;
return this.index < this.sequence.length();
}
public boolean hasMore(final int offset) {
return this.index + offset < this.sequence.length();
}
/**
* Search for the provided token, and advance the reader index past the {@code until} character.
*
@ -109,6 +116,23 @@ package com.viaversion.viaversion.api.minecraft.nbt;
return this;
}
/**
* If the next non-whitespace character is {@code token}, advance past it.
*
* <p>This method always consumes whitespace.</p>
*
* @param token next non-whitespace character to query
* @return if the next non-whitespace character is {@code token}
*/
public boolean takeIf(final char token) {
this.skipWhitespace();
if (this.hasMore() && this.peek() == token) {
this.advance();
return true;
}
return false;
}
public CharBuffer skipWhitespace() {
while (this.hasMore() && Character.isWhitespace(this.peek())) this.advance();
return this;

View File

@ -26,9 +26,9 @@ package com.viaversion.viaversion.api.minecraft.nbt;
import java.io.IOException;
/**
* An exception thrown when parsing a string tag
* An exception thrown when parsing a string tag.
*/
/* package */ class StringTagParseException extends IOException {
class StringTagParseException extends IOException {
private static final long serialVersionUID = -3001637554903912905L;
private final CharSequence buffer;
private final int position;

View File

@ -1,7 +1,7 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2020 KyoriPowered
* Copyright (c) 2017-2021 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -37,33 +37,39 @@ import com.github.steveice10.opennbt.tag.builtin.NumberTag;
import com.github.steveice10.opennbt.tag.builtin.ShortTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
// Specific Via changes:
// - Use OpenNBT tags
// - Small byteArray() optimization
// - acceptLegacy = true by default
final class TagStringReader {
private static final int MAX_DEPTH = 512;
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static final int[] EMPTY_INT_ARRAY = new int[0];
private static final long[] EMPTY_LONG_ARRAY = new long[0];
/**
* See https://github.com/KyoriPowered/adventure.
*/
/* package */ final class TagStringReader {
private final CharBuffer buffer;
private boolean acceptLegacy = true;
private int depth;
public TagStringReader(final CharBuffer buffer) {
TagStringReader(final CharBuffer buffer) {
this.buffer = buffer;
}
public CompoundTag compound() throws StringTagParseException {
this.buffer.expect(Tokens.COMPOUND_BEGIN);
final CompoundTag compoundTag = new CompoundTag();
if (this.buffer.peek() == Tokens.COMPOUND_END) {
this.buffer.take();
if (this.buffer.takeIf(Tokens.COMPOUND_END)) {
return compoundTag;
}
while (this.buffer.hasMore()) {
final String key = this.key();
final Tag tag = this.tag();
compoundTag.put(key, tag);
compoundTag.put(this.key(), this.tag());
if (this.separatorOrCompleteWith(Tokens.COMPOUND_END)) {
return compoundTag;
}
@ -74,13 +80,11 @@ import java.util.stream.IntStream;
public ListTag list() throws StringTagParseException {
final ListTag listTag = new ListTag();
this.buffer.expect(Tokens.ARRAY_BEGIN);
final boolean prefixedIndex = this.buffer.peek() == '0' && this.buffer.peek(1) == ':';
final boolean prefixedIndex = this.acceptLegacy && this.buffer.peek() == '0' && this.buffer.peek(1) == ':';
if (!prefixedIndex && this.buffer.takeIf(Tokens.ARRAY_END)) {
return listTag;
}
while (this.buffer.hasMore()) {
if (this.buffer.peek() == Tokens.ARRAY_END) {
this.buffer.advance();
return listTag;
}
if (prefixedIndex) {
this.buffer.takeUntil(':');
}
@ -99,11 +103,12 @@ import java.util.stream.IntStream;
*
* @return array-typed tag
*/
public Tag array(final char elementType) throws StringTagParseException {
public Tag array(char elementType) throws StringTagParseException {
this.buffer.expect(Tokens.ARRAY_BEGIN)
.expect(elementType)
.expect(Tokens.ARRAY_SIGNATURE_SEPARATOR);
elementType = Character.toLowerCase(elementType);
if (elementType == Tokens.TYPE_BYTE) {
return new ByteArrayTag(this.byteArray());
} else if (elementType == Tokens.TYPE_INT) {
@ -116,11 +121,15 @@ import java.util.stream.IntStream;
}
private byte[] byteArray() throws StringTagParseException {
final List<Byte> bytes = new ArrayList<>();
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
return EMPTY_BYTE_ARRAY;
}
final IntList bytes = new IntArrayList();
while (this.buffer.hasMore()) {
final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_BYTE);
try {
bytes.add(Byte.valueOf(value.toString()));
bytes.add(Byte.parseByte(value.toString()));
} catch (final NumberFormatException ex) {
throw this.buffer.makeError("All elements of a byte array must be bytes!");
}
@ -128,7 +137,7 @@ import java.util.stream.IntStream;
if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) {
final byte[] result = new byte[bytes.size()];
for (int i = 0; i < bytes.size(); ++i) {
result[i] = bytes.get(i);
result[i] = (byte) bytes.getInt(i);
}
return result;
}
@ -137,6 +146,10 @@ import java.util.stream.IntStream;
}
private int[] intArray() throws StringTagParseException {
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
return EMPTY_INT_ARRAY;
}
final IntStream.Builder builder = IntStream.builder();
while (this.buffer.hasMore()) {
final Tag value = this.tag();
@ -152,21 +165,21 @@ import java.util.stream.IntStream;
}
private long[] longArray() throws StringTagParseException {
final List<Long> longs = new ArrayList<>();
if (this.buffer.takeIf(Tokens.ARRAY_END)) {
return EMPTY_LONG_ARRAY;
}
final LongStream.Builder longs = LongStream.builder();
while (this.buffer.hasMore()) {
final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_LONG);
try {
longs.add(Long.valueOf(value.toString()));
longs.add(Long.parseLong(value.toString()));
} catch (final NumberFormatException ex) {
throw this.buffer.makeError("All elements of a long array must be longs!");
}
if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) {
final long[] result = new long[longs.size()];
for (int i = 0; i < longs.size(); ++i) {
result[i] = longs.get(i);
}
return result;
return longs.build().toArray();
}
}
throw this.buffer.makeError("Reached end of document without array close");
@ -181,7 +194,21 @@ import java.util.stream.IntStream;
}
final StringBuilder builder = new StringBuilder();
while (this.buffer.peek() != ':') { // DO NOT CHECK FOR CHARACTER VALIDITY; LEGACY NBT ALLOWS ANY CHARACTER, EVEN WHEN UNQUOTED
while (this.buffer.hasMore()) {
final char peek = this.buffer.peek();
if (!Tokens.id(peek)) {
if (this.acceptLegacy) {
// In legacy format, a key is any non-colon character, with escapes allowed
if (peek == Tokens.ESCAPE_MARKER) {
this.buffer.take(); // skip
continue;
} else if (peek != Tokens.COMPOUND_KEY_TERMINATOR) {
builder.append(this.buffer.take());
continue;
}
}
break;
}
builder.append(this.buffer.take());
}
return builder.toString();
@ -191,30 +218,38 @@ import java.util.stream.IntStream;
}
public Tag tag() throws StringTagParseException {
final char startToken = this.buffer.skipWhitespace().peek();
switch (startToken) {
case Tokens.COMPOUND_BEGIN:
return this.compound();
case Tokens.ARRAY_BEGIN:
if (this.buffer.peek(2) == ';') { // we know we're an array tag
return this.array(this.buffer.peek(1));
} else {
return this.list();
}
case Tokens.SINGLE_QUOTE:
case Tokens.DOUBLE_QUOTE:
// definitely a string tag
this.buffer.advance();
return new StringTag(unescape(this.buffer.takeUntil(startToken).toString()));
default: // scalar
return this.scalar();
if (this.depth++ > MAX_DEPTH) {
throw this.buffer.makeError("Exceeded maximum allowed depth of " + MAX_DEPTH + " when reading tag");
}
try {
final char startToken = this.buffer.skipWhitespace().peek();
switch (startToken) {
case Tokens.COMPOUND_BEGIN:
return this.compound();
case Tokens.ARRAY_BEGIN:
// Maybe add in a legacy-only mode to read those?
if (this.buffer.hasMore(2) && this.buffer.peek(2) == ';') { // we know we're an array tag
return this.array(this.buffer.peek(1));
} else {
return this.list();
}
case Tokens.SINGLE_QUOTE:
case Tokens.DOUBLE_QUOTE:
// definitely a string tag
this.buffer.advance();
return new StringTag(unescape(this.buffer.takeUntil(startToken).toString()));
default: // scalar
return this.scalar();
}
} finally {
this.depth--;
}
}
/**
* A tag that is definitely some sort of scalar
* A tag that is definitely some sort of scalar.
*
* <p>Does not detect quoted strings, so </p>
* <p>Does not detect quoted strings, so those should have been parsed already.</p>
*
* @return a parsed tag
*/
@ -227,7 +262,7 @@ import java.util.stream.IntStream;
if (builder.length() != 0) {
Tag result = null;
try {
switch (Character.toUpperCase(current)) { // try to read and return as a number
switch (Character.toLowerCase(current)) { // try to read and return as a number
// case Tokens.TYPE_INTEGER: // handled below, ints are ~special~
case Tokens.TYPE_BYTE:
result = new ByteTag(Byte.parseByte(builder.toString()));
@ -269,29 +304,33 @@ import java.util.stream.IntStream;
try {
return new IntTag(Integer.parseInt(built));
} catch (final NumberFormatException ex) {
// ignore
try {
return new DoubleTag(Double.parseDouble(built));
} catch (final NumberFormatException ex2) {
// ignore
}
}
}
if (built.equalsIgnoreCase(Tokens.LITERAL_TRUE)) {
return new ByteTag((byte) 1);
} else if (built.equalsIgnoreCase(Tokens.LITERAL_FALSE)) {
return new ByteTag((byte) 0);
}
return new StringTag(built);
}
private boolean separatorOrCompleteWith(final char endCharacter) throws StringTagParseException {
if (this.buffer.skipWhitespace().peek() == endCharacter) {
this.buffer.take();
if (this.buffer.takeIf(endCharacter)) {
return true;
}
this.buffer.expect(Tokens.VALUE_SEPARATOR);
if (this.buffer.skipWhitespace().peek() == endCharacter) {
this.buffer.take();
return true;
}
return false;
}
/**
* Remove simple escape sequences from a string
* Remove simple escape sequences from a string.
*
* @param withEscapes input string with escapes
* @return string with escapes processed
@ -310,4 +349,8 @@ import java.util.stream.IntStream;
output.append(withEscapes.substring(lastEscape));
return output.toString();
}
public void legacy(final boolean acceptLegacy) {
this.acceptLegacy = acceptLegacy;
}
}

View File

@ -33,6 +33,7 @@ import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.LongArrayTag;
import com.github.steveice10.opennbt.tag.builtin.LongTag;
import com.github.steveice10.opennbt.tag.builtin.NumberTag;
import com.github.steveice10.opennbt.tag.builtin.ShortTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
@ -41,12 +42,16 @@ import java.io.IOException;
import java.io.Writer;
import java.util.Map;
// Specific Via changes:
// - Use OpenNBT tags
// - Has not been updated to support pretty printing and legacy writing since that is not needed
/**
* See https://github.com/KyoriPowered/adventure.
* An emitter for the SNBT format.
*
* <p>Details on the format are described in the package documentation.</p>
*/
/* package */ final class TagStringWriter implements AutoCloseable {
final class TagStringWriter implements AutoCloseable {
private final Appendable out;
private final String indent = " ";
private int level;
/**
* Whether a {@link Tokens#VALUE_SEPARATOR} needs to be printed before the beginning of the next object.
@ -73,17 +78,17 @@ import java.util.Map;
} else if (tag instanceof StringTag) {
return this.value(((StringTag) tag).getValue(), Tokens.EOF);
} else if (tag instanceof ByteTag) {
return this.value(Byte.toString(((ByteTag) tag).asByte()), Tokens.TYPE_BYTE);
return this.value(Byte.toString(((NumberTag) tag).asByte()), Tokens.TYPE_BYTE);
} else if (tag instanceof ShortTag) {
return this.value(Short.toString(((ShortTag) tag).asShort()), Tokens.TYPE_SHORT);
return this.value(Short.toString(((NumberTag) tag).asShort()), Tokens.TYPE_SHORT);
} else if (tag instanceof IntTag) {
return this.value(Integer.toString(((IntTag) tag).asInt()), Tokens.TYPE_INT);
return this.value(Integer.toString(((NumberTag) tag).asInt()), Tokens.TYPE_INT);
} else if (tag instanceof LongTag) {
return this.value(Long.toString(((LongTag) tag).asLong()), Tokens.TYPE_LONG);
return this.value(Long.toString(((NumberTag) tag).asLong()), Character.toUpperCase(Tokens.TYPE_LONG)); // special case
} else if (tag instanceof FloatTag) {
return this.value(Float.toString(((FloatTag) tag).asFloat()), Tokens.TYPE_FLOAT);
return this.value(Float.toString(((NumberTag) tag).asFloat()), Tokens.TYPE_FLOAT);
} else if (tag instanceof DoubleTag) {
return this.value(Double.toString(((DoubleTag) tag).asDouble()), Tokens.TYPE_DOUBLE);
return this.value(Double.toString(((NumberTag) tag).asDouble()), Tokens.TYPE_DOUBLE);
} else {
throw new IOException("Unknown tag type: " + tag.getClass().getSimpleName());
// unknown!
@ -92,7 +97,7 @@ import java.util.Map;
private TagStringWriter writeCompound(final CompoundTag tag) throws IOException {
this.beginCompound();
for (Map.Entry<String, Tag> entry : tag.entrySet()) {
for (final Map.Entry<String, Tag> entry : tag.entrySet()) {
this.key(entry.getKey());
this.writeTag(entry.getValue());
}
@ -244,7 +249,6 @@ import java.util.Map;
}
}
@Override
public void close() throws IOException {
if (this.level != 0) {

View File

@ -1,7 +1,7 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2020 KyoriPowered
* Copyright (c) 2017-2021 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -23,10 +23,7 @@
*/
package com.viaversion.viaversion.api.minecraft.nbt;
/**
* See https://github.com/KyoriPowered/adventure.
*/
/* package */ final class Tokens {
final class Tokens {
// Compounds
static final char COMPOUND_BEGIN = '{';
static final char COMPOUND_END = '}';
@ -43,20 +40,24 @@ package com.viaversion.viaversion.api.minecraft.nbt;
static final char DOUBLE_QUOTE = '"';
static final char ESCAPE_MARKER = '\\';
static final char TYPE_BYTE = 'B';
static final char TYPE_SHORT = 'S';
static final char TYPE_INT = 'I'; // array only
static final char TYPE_LONG = 'L';
static final char TYPE_FLOAT = 'F';
static final char TYPE_DOUBLE = 'D';
static final char TYPE_BYTE = 'b';
static final char TYPE_SHORT = 's';
static final char TYPE_INT = 'i'; // array only
static final char TYPE_LONG = 'l';
static final char TYPE_FLOAT = 'f';
static final char TYPE_DOUBLE = 'd';
static final String LITERAL_TRUE = "true";
static final String LITERAL_FALSE = "false";
static final String NEWLINE = System.getProperty("line.separator", "\n");
static final char EOF = '\0';
private Tokens() {
}
/**
* Return if a character is a valid component in an identifier
* Return if a character is a valid component in an identifier.
*
* <p>An identifier character must match the expression {@code [a-zA-Z0-9_+.-]}</p>
*
@ -73,8 +74,8 @@ package com.viaversion.viaversion.api.minecraft.nbt;
/**
* Return whether a character could be at some position in a number.
* <p>
* A string passing this check does not necessarily mean it is syntactically valid
*
* <p>A string passing this check does not necessarily mean it is syntactically valid.</p>
*
* @param c character to check
* @return if possibly part of a number