diff --git a/nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java b/nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java index ad847870c0..00b447540c 100644 --- a/nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java +++ b/nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java @@ -27,9 +27,13 @@ import java.util.List; import java.util.stream.IntStream; import java.util.stream.LongStream; +import org.jetbrains.annotations.Nullable; final class TagStringReader { private static final int MAX_DEPTH = 512; + private static final int HEX_RADIX = 16; + private static final int BINARY_RADIX = 2; + private static final int DECIMAL_RADIX = 10; 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]; @@ -238,9 +242,8 @@ public BinaryTag tag() throws StringTagParseException { * * @return a parsed tag */ - private BinaryTag scalar() { + private BinaryTag scalar() throws StringTagParseException { final StringBuilder builder = new StringBuilder(); - int noLongerNumericAt = -1; while (this.buffer.hasMore()) { char current = this.buffer.peek(); if (current == '\\') { // escape -- we are significantly more lenient than original format at the moment @@ -252,48 +255,59 @@ private BinaryTag scalar() { break; } builder.append(current); - if (noLongerNumericAt == -1 && !Tokens.numeric(current)) { - noLongerNumericAt = builder.length(); + } + if (builder.length() == 0) { + throw this.buffer.makeError("Expected a value but got nothing"); + } + final String original = builder.toString(); // use unmodified string when number parsing fails + + // Start stripping down the string so we can use Java's number parsing instead of having to write our own. + // Determine the radix and strip its prefix if present + final int radix = this.extractRadix(builder, original); + + // Check for the sign before removing the type token because of hex number always needing a sign thanks to byte types + final char last = builder.charAt(builder.length() - 1); + boolean hasSignToken = false; + boolean signed = radix != HEX_RADIX; // hex defaults to unsigned + if (builder.length() > 2) { + final char signChar = builder.charAt(builder.length() - 2); + if (signChar == Tokens.TYPE_SIGNED || signChar == Tokens.TYPE_UNSIGNED) { + hasSignToken = true; + signed = signChar == Tokens.TYPE_SIGNED; + builder.deleteCharAt(builder.length() - 2); } } - final int length = builder.length(); - final String built = builder.toString(); - if (noLongerNumericAt == length && length > 1) { - final char last = built.charAt(length - 1); + // Check for the type token and make sure we didn't fall into the hex trap (e.g. 0xAB) + boolean hasTypeToken = false; + char typeToken = Tokens.TYPE_INT; + if (Tokens.numericType(last) && (hasSignToken || radix != HEX_RADIX)) { + hasTypeToken = true; + typeToken = Character.toLowerCase(last); + builder.deleteCharAt(builder.length() - 1); + } + + if (!signed && (typeToken == Tokens.TYPE_FLOAT || typeToken == Tokens.TYPE_DOUBLE)) { + throw this.buffer.makeError("Cannot create unsigned floating point numbers"); + } + + final String strippedString = builder.toString().replace("_", ""); + if (hasTypeToken) { try { - switch (Character.toLowerCase(last)) { // try to read and return as a number - case Tokens.TYPE_BYTE: - return ByteBinaryTag.byteBinaryTag(Byte.parseByte(built.substring(0, length - 1))); - case Tokens.TYPE_SHORT: - return ShortBinaryTag.shortBinaryTag(Short.parseShort(built.substring(0, length - 1))); - case Tokens.TYPE_INT: - return IntBinaryTag.intBinaryTag(Integer.parseInt(built.substring(0, length - 1))); - case Tokens.TYPE_LONG: - return LongBinaryTag.longBinaryTag(Long.parseLong(built.substring(0, length - 1))); - case Tokens.TYPE_FLOAT: - final float floatValue = Float.parseFloat(built.substring(0, length - 1)); - if (Float.isFinite(floatValue)) { // don't accept NaN and Infinity - return FloatBinaryTag.floatBinaryTag(floatValue); - } - break; - case Tokens.TYPE_DOUBLE: - final double doubleValue = Double.parseDouble(built.substring(0, length - 1)); - if (Double.isFinite(doubleValue)) { // don't accept NaN and Infinity - return DoubleBinaryTag.doubleBinaryTag(doubleValue); - } - break; + final NumberBinaryTag tag = this.parseNumberTag(strippedString, typeToken, radix, signed); + if (tag != null) { + return tag; } } catch (final NumberFormatException ignored) { // not a numeric tag of the appropriate type } - } else if (noLongerNumericAt == -1) { // 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 + } else { // default to int or double parsing before falling back to string try { - return IntBinaryTag.intBinaryTag(Integer.parseInt(built)); + return IntBinaryTag.intBinaryTag(this.parseInt(strippedString, radix, signed)); } catch (final NumberFormatException ex) { - if (built.indexOf('.') != -1) { // see if we have an unsuffixed double; always needs a dot + if (strippedString.indexOf('.') != -1) { // see if we have an unsuffixed double; always needs a dot try { - return DoubleBinaryTag.doubleBinaryTag(Double.parseDouble(built)); + return DoubleBinaryTag.doubleBinaryTag(Double.parseDouble(strippedString)); } catch (final NumberFormatException ex2) { // ignore } @@ -301,13 +315,88 @@ private BinaryTag scalar() { } } - if (built.equalsIgnoreCase(Tokens.LITERAL_TRUE)) { + if (original.equalsIgnoreCase(Tokens.LITERAL_TRUE)) { return ByteBinaryTag.ONE; - } else if (built.equalsIgnoreCase(Tokens.LITERAL_FALSE)) { + } else if (original.equalsIgnoreCase(Tokens.LITERAL_FALSE)) { return ByteBinaryTag.ZERO; } - return StringBinaryTag.stringBinaryTag(built); + return StringBinaryTag.stringBinaryTag(original); + } + + private int extractRadix(final StringBuilder builder, final String original) { + int radixPrefixOffset = 0; + final int radix; + final char first = builder.charAt(0); + if (first == '+' || first == '-') { + radixPrefixOffset = 1; + } + if (original.startsWith("0b", radixPrefixOffset) || original.startsWith("0B", radixPrefixOffset)) { + radix = BINARY_RADIX; + } else if (original.startsWith("0x", radixPrefixOffset) || original.startsWith("0X", radixPrefixOffset)) { + radix = HEX_RADIX; + } else { + radix = DECIMAL_RADIX; + } + if (radix != DECIMAL_RADIX) { + builder.delete(radixPrefixOffset, 2 + radixPrefixOffset); + } + return radix; + } + + private @Nullable NumberBinaryTag parseNumberTag(final String s, final char typeToken, final int radix, final boolean signed) { + switch (typeToken) { + case Tokens.TYPE_BYTE: + return ByteBinaryTag.byteBinaryTag(this.parseByte(s, radix, signed)); + case Tokens.TYPE_SHORT: + return ShortBinaryTag.shortBinaryTag(this.parseShort(s, radix, signed)); + case Tokens.TYPE_INT: + return IntBinaryTag.intBinaryTag(this.parseInt(s, radix, signed)); + case Tokens.TYPE_LONG: + return LongBinaryTag.longBinaryTag(this.parseLong(s, radix, signed)); + case Tokens.TYPE_FLOAT: + final float floatValue = Float.parseFloat(s); + if (Float.isFinite(floatValue)) { // don't accept NaN and Infinity + return FloatBinaryTag.floatBinaryTag(floatValue); + } + break; + case Tokens.TYPE_DOUBLE: + final double doubleValue = Double.parseDouble(s); + if (Double.isFinite(doubleValue)) { // don't accept NaN and Infinity + return DoubleBinaryTag.doubleBinaryTag(doubleValue); + } + break; + } + return null; + } + + private byte parseByte(final String s, final int radix, final boolean signed) { + if (signed) { + return Byte.parseByte(s, radix); + } + final int parsedInt = Integer.parseInt(s, radix); + if (parsedInt >> Byte.SIZE == 0) { + return (byte) parsedInt; + } + throw new NumberFormatException(); + } + + private short parseShort(final String s, final int radix, final boolean signed) { + if (signed) { + return Short.parseShort(s, radix); + } + final int parsedInt = Integer.parseInt(s, radix); + if (parsedInt >> Short.SIZE == 0) { + return (short) parsedInt; + } + throw new NumberFormatException(); + } + + private int parseInt(final String s, final int radix, final boolean signed) { + return signed ? Integer.parseInt(s, radix) : Integer.parseUnsignedInt(s, radix); + } + private long parseLong(final String s, final int radix, final boolean signed) { + return signed ? Long.parseLong(s, radix) : Long.parseUnsignedLong(s, radix); } private boolean separatorOrCompleteWith(final char endCharacter) throws StringTagParseException { diff --git a/nbt/src/main/java/net/kyori/adventure/nbt/Tokens.java b/nbt/src/main/java/net/kyori/adventure/nbt/Tokens.java index 5a34fffe28..9d4f43d344 100644 --- a/nbt/src/main/java/net/kyori/adventure/nbt/Tokens.java +++ b/nbt/src/main/java/net/kyori/adventure/nbt/Tokens.java @@ -47,6 +47,9 @@ final class Tokens { static final char TYPE_FLOAT = 'f'; static final char TYPE_DOUBLE = 'd'; + static final char TYPE_SIGNED = 's'; + static final char TYPE_UNSIGNED = 'u'; + static final String LITERAL_TRUE = "true"; static final String LITERAL_FALSE = "false"; @@ -73,17 +76,18 @@ static boolean id(final char c) { } /** - * 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.
+ * Return whether a character is a numeric type identifier. * * @param c character to check - * @return if possibly part of a number + * @return if a numeric type identifier */ - static boolean numeric(final char c) { - return (c >= '0' && c <= '9') // digit - || c == '+' || c == '-' // positive or negative - || c == 'e' || c == 'E' // exponent - || c == '.'; // decimal + static boolean numericType(char c) { + c = Character.toLowerCase(c); + return c == TYPE_BYTE + || c == TYPE_SHORT + || c == TYPE_INT + || c == TYPE_LONG + || c == TYPE_FLOAT + || c == TYPE_DOUBLE; } } diff --git a/nbt/src/test/java/net/kyori/adventure/nbt/StringIOTest.java b/nbt/src/test/java/net/kyori/adventure/nbt/StringIOTest.java index 4c0f3a8d2a..1ebe772078 100644 --- a/nbt/src/test/java/net/kyori/adventure/nbt/StringIOTest.java +++ b/nbt/src/test/java/net/kyori/adventure/nbt/StringIOTest.java @@ -153,7 +153,17 @@ void testIntTag() throws IOException { assertEquals("448228", this.tagToString(IntBinaryTag.intBinaryTag(448228))); assertEquals(IntBinaryTag.intBinaryTag(4482828), this.stringToTag("4482828")); + assertEquals(IntBinaryTag.intBinaryTag(4482828), this.stringToTag("4_4_8______2_8_2_8")); assertEquals(IntBinaryTag.intBinaryTag(-24), this.stringToTag("-24")); + assertEquals(IntBinaryTag.intBinaryTag(0xABC), this.stringToTag("0xABC")); + assertEquals(IntBinaryTag.intBinaryTag(0b1001), this.stringToTag("0b1001")); + } + + @Test + void testNumberSign() throws IOException { + assertEquals(ByteBinaryTag.byteBinaryTag((byte) -16), this.stringToTag("240ub")); + assertEquals(ByteBinaryTag.byteBinaryTag((byte) -16), this.stringToTag("-16sb")); + assertEquals(IntBinaryTag.intBinaryTag(-0xABC), this.stringToTag("-0xABCsI")); } @Test @@ -180,6 +190,7 @@ void testFloatTag() throws IOException { assertEquals(FloatBinaryTag.floatBinaryTag(-4.3e-4f), this.stringToTag("-4.3e-4F")); assertEquals(FloatBinaryTag.floatBinaryTag(4.3e-4f), this.stringToTag("+4.3e-4F")); assertEquals(FloatBinaryTag.floatBinaryTag(0.3f), this.stringToTag(".3F")); + assertEquals(FloatBinaryTag.floatBinaryTag(3.0f), this.stringToTag("3.F")); } @Test @@ -190,6 +201,8 @@ void testDoubleTag() throws IOException { assertEquals(DoubleBinaryTag.doubleBinaryTag(4.3e-4d), this.stringToTag("4.3e-4d")); assertEquals(DoubleBinaryTag.doubleBinaryTag(-4.3e-4d), this.stringToTag("-4.3e-4D")); assertEquals(DoubleBinaryTag.doubleBinaryTag(4.3e-4d), this.stringToTag("+4.3e-4D")); + assertEquals(DoubleBinaryTag.doubleBinaryTag(3.0d), this.stringToTag("3.")); + assertEquals(DoubleBinaryTag.doubleBinaryTag(0.3d), this.stringToTag(".3")); } @Test