/* * This file is part of adventure, licensed under the MIT License. * * Copyright (c) 2017-2020 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 * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package us.myles.ViaVersion.api.minecraft.nbt; import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; import com.github.steveice10.opennbt.tag.builtin.ByteTag; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.DoubleTag; import com.github.steveice10.opennbt.tag.builtin.FloatTag; import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; 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.ShortTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.stream.IntStream; /** * See https://github.com/KyoriPowered/adventure. */ /* package */ final class TagStringReader { private static final Field NAME_FIELD = getNameField(); private final CharBuffer buffer; private static Field getNameField() { try { return Tag.class.getDeclaredField("name"); } catch (NoSuchFieldException e) { e.printStackTrace(); throw new IllegalArgumentException(e); } } public TagStringReader(final CharBuffer buffer) { this.buffer = buffer; } public CompoundTag compound() throws StringTagParseException { this.buffer.expect(Tokens.COMPOUND_BEGIN); final CompoundTag compoundTag = new CompoundTag(""); while (this.buffer.hasMore()) { final String key = this.key(); final Tag tag = this.tag(); // Doesn't get around this with the steveice lib :/ try { if (!NAME_FIELD.isAccessible()) { NAME_FIELD.setAccessible(true); } NAME_FIELD.set(tag, key); } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } compoundTag.put(tag); if (this.separatorOrCompleteWith(Tokens.COMPOUND_END)) { return compoundTag; } } throw this.buffer.makeError("Unterminated compound tag!"); } 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) == ':'; while (this.buffer.hasMore()) { if (prefixedIndex) { this.buffer.takeUntil(':'); } final Tag next = this.tag(); // TODO: validate type listTag.add(next); if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { return listTag; } } throw this.buffer.makeError("Reached end of file without end of list tag!"); } /** * Similar to a list tag in syntax, but returning a single array tag rather than a list of tags. * * @return array-typed tag */ public Tag array(final char elementType) throws StringTagParseException { this.buffer.expect(Tokens.ARRAY_BEGIN) .expect(elementType) .expect(Tokens.ARRAY_SIGNATURE_SEPARATOR); if (elementType == Tokens.TYPE_BYTE) { return new ByteArrayTag("", this.byteArray()); } else if (elementType == Tokens.TYPE_INT) { return new IntArrayTag("", this.intArray()); } else if (elementType == Tokens.TYPE_LONG) { return new LongArrayTag("", this.longArray()); } else { throw this.buffer.makeError("Type " + elementType + " is not a valid element type in an array!"); } } private byte[] byteArray() throws StringTagParseException { final List<Byte> bytes = new ArrayList<>(); while (this.buffer.hasMore()) { final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_BYTE); try { bytes.add(Byte.valueOf(value.toString())); } catch (final NumberFormatException ex) { throw this.buffer.makeError("All elements of a byte array must be bytes!"); } if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { final byte[] result = new byte[bytes.size()]; for (int i = 0; i < bytes.size(); ++i) { // todo yikes, let's do less boxing result[i] = bytes.get(i); } return result; } } throw this.buffer.makeError("Reached end of document without array close"); } private int[] intArray() throws StringTagParseException { final IntStream.Builder builder = IntStream.builder(); while (this.buffer.hasMore()) { final Tag value = this.tag(); if (!(value instanceof IntTag)) { throw this.buffer.makeError("All elements of an int array must be ints!"); } builder.add(((IntTag) value).getValue()); if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { return builder.build().toArray(); } } throw this.buffer.makeError("Reached end of document without array close"); } private long[] longArray() throws StringTagParseException { final List<Long> longs = new ArrayList<>(); while (this.buffer.hasMore()) { final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_LONG); try { longs.add(Long.valueOf(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) { // todo yikes result[i] = longs.get(i); } return result; } } throw this.buffer.makeError("Reached end of document without array close"); } public String key() throws StringTagParseException { this.buffer.skipWhitespace(); final char starChar = this.buffer.peek(); try { if (starChar == Tokens.SINGLE_QUOTE || starChar == Tokens.DOUBLE_QUOTE) { return unescape(this.buffer.takeUntil(this.buffer.take()).toString()); } final StringBuilder builder = new StringBuilder(); while (Tokens.id(this.buffer.peek())) { builder.append(this.buffer.take()); } return builder.toString(); } finally { this.buffer.expect(Tokens.COMPOUND_KEY_TERMINATOR); } } 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(); } } /** * A tag that is definitely some sort of scalar * * <p>Does not detect quoted strings, so </p> * * @return a parsed tag */ private Tag scalar() { final StringBuilder builder = new StringBuilder(); boolean possiblyNumeric = true; while (this.buffer.hasMore()) { final char current = this.buffer.peek(); if (possiblyNumeric && !Tokens.numeric(current)) { if (builder.length() != 0) { Tag result = null; try { switch (Character.toUpperCase(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())); break; case Tokens.TYPE_SHORT: result = new ShortTag("", (Short.parseShort(builder.toString()))); break; case Tokens.TYPE_LONG: result = new LongTag("", (Long.parseLong(builder.toString()))); break; case Tokens.TYPE_FLOAT: result = new FloatTag("", (Float.parseFloat(builder.toString()))); break; case Tokens.TYPE_DOUBLE: result = new DoubleTag("", (Double.parseDouble(builder.toString()))); break; } } catch (final NumberFormatException ex) { possiblyNumeric = false; // fallback to treating as a String } if (result != null) { this.buffer.take(); return result; } } } if (current == '\\') { // escape -- we are significantly more lenient than original format at the moment this.buffer.advance(); builder.append(this.buffer.take()); } else if (Tokens.id(current)) { builder.append(this.buffer.take()); } else { // end of value break; } } // if we run out of content without an explicit value separator, then we're either an integer or string tag -- all others have a character at the end final String built = builder.toString(); if (possiblyNumeric) { try { return new IntTag("", Integer.parseInt(built)); } catch (final NumberFormatException ex) { // ignore } } return new StringTag("", built); } private boolean separatorOrCompleteWith(final char endCharacter) throws StringTagParseException { if (this.buffer.skipWhitespace().peek() == endCharacter) { this.buffer.take(); return true; } this.buffer.expect(Tokens.VALUE_SEPARATOR); return false; } /** * Remove simple escape sequences from a string * * @param withEscapes input string with escapes * @return string with escapes processed */ private static String unescape(final String withEscapes) { int escapeIdx = withEscapes.indexOf(Tokens.ESCAPE_MARKER); if (escapeIdx == -1) { // nothing to unescape return withEscapes; } int lastEscape = 0; final StringBuilder output = new StringBuilder(withEscapes.length()); do { output.append(withEscapes, lastEscape, escapeIdx); lastEscape = escapeIdx + 1; } while ((escapeIdx = withEscapes.indexOf(Tokens.ESCAPE_MARKER, lastEscape + 1)) != -1); // add one extra character to make sure we don't include escaped backslashes output.append(withEscapes.substring(lastEscape)); return output.toString(); } }