diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/BinaryTagIO.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/BinaryTagIO.java index 0b0b1c06e..d7a755cb3 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/BinaryTagIO.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/BinaryTagIO.java @@ -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() { diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/CharBuffer.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/CharBuffer.java index 2d4ad1c9e..83bf69a07 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/CharBuffer.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/CharBuffer.java @@ -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. + * + *

This method always consumes whitespace.

+ * + * @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; diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/StringTagParseException.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/StringTagParseException.java index 57e6d633c..198086342 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/StringTagParseException.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/StringTagParseException.java @@ -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; diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringReader.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringReader.java index 6a17af19e..baef70a00 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringReader.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringReader.java @@ -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 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 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. * - *

Does not detect quoted strings, so

+ *

Does not detect quoted strings, so those should have been parsed already.

* * @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; + } } diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringWriter.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringWriter.java index 32b67ae13..57706fc78 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringWriter.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/TagStringWriter.java @@ -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. + * + *

Details on the format are described in the package documentation.

*/ -/* 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 entry : tag.entrySet()) { + for (final Map.Entry 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) { diff --git a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/Tokens.java b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/Tokens.java index 7dfcce77a..80d37fea1 100644 --- a/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/Tokens.java +++ b/api/src/main/java/com/viaversion/viaversion/api/minecraft/nbt/Tokens.java @@ -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. * *

An identifier character must match the expression {@code [a-zA-Z0-9_+.-]}

* @@ -73,8 +74,8 @@ package com.viaversion.viaversion.api.minecraft.nbt; /** * Return whether a character could be at some position in a number. - *

- * A string passing this check does not necessarily mean it is syntactically valid + * + *

A string passing this check does not necessarily mean it is syntactically valid.

* * @param c character to check * @return if possibly part of a number