Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 125 additions & 36 deletions nbt/src/main/java/net/kyori/adventure/nbt/TagStringReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand All @@ -252,62 +255,148 @@ 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
}
}
}
}

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 {
Expand Down
22 changes: 13 additions & 9 deletions nbt/src/main/java/net/kyori/adventure/nbt/Tokens.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -73,17 +76,18 @@ static boolean id(final char c) {
}

/**
* 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>
* 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;
}
}
13 changes: 13 additions & 0 deletions nbt/src/test/java/net/kyori/adventure/nbt/StringIOTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down