diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 50f6c48d0..6fbecf2bc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -62,6 +62,17 @@ public enum ProtocolUtils { private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB private static final QuietDecoderException BAD_VARINT_CACHED = new QuietDecoderException("Bad varint decoded"); + private static final int[] VAR_INT_BYTE_LENGTHS = new int[33]; + + static { + // Inspired by https://richardstartin.github.io/posts/dont-use-protobuf-for-telemetry + // This has been slightly modified in that we reduce the length to 32-bit only, since Velocity + // doesn't look at any part of the Minecraft protocol that requires us to look at VarLongs. + for (int i = 0; i <= 32; ++i) { + VAR_INT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d); + } + VAR_INT_BYTE_LENGTHS[32] = 1; // Special case for the number 0. + } /** * Reads a Minecraft-style VarInt from the specified {@code buf}. @@ -97,21 +108,22 @@ public enum ProtocolUtils { return Integer.MIN_VALUE; } + public static int varintBytes(int value) { + return VAR_INT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)]; + } + /** * Writes a Minecraft-style VarInt to the specified {@code buf}. * @param buf the buffer to read from * @param value the integer to write */ public static void writeVarInt(ByteBuf buf, int value) { - while (true) { - if ((value & 0xFFFFFF80) == 0) { - buf.writeByte(value); - return; - } - - buf.writeByte(value & 0x7F | 0x80); + int length = varintBytes(value); + for (int i = 0; i < length; ++i) { + buf.writeByte(((byte) ((value & 0x7F) | 0x80))); value >>>= 7; } + buf.writeByte((byte) value); } public static String readString(ByteBuf buf) { diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java new file mode 100644 index 000000000..35431d81a --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java @@ -0,0 +1,42 @@ +package com.velocitypowered.proxy.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ProtocolUtilsTest { + + @Test + void negativeVarIntBytes() { + assertEquals(5, ProtocolUtils.varintBytes(-1)); + assertEquals(5, ProtocolUtils.varintBytes(Integer.MIN_VALUE)); + } + + @Test + void zeroVarIntBytes() { + assertEquals(1, ProtocolUtils.varintBytes(0)); + assertEquals(1, ProtocolUtils.varintBytes(1)); + } + + @Test + void ensureConsistencyAcrossNumberBits() { + for (int i = 0; i <= 31; i++) { + int number = (1 << i) - 1; + assertEquals(conventionalWrittenBytes(number), ProtocolUtils.varintBytes(number), + "mismatch with " + i + "-bit number"); + } + } + + private int conventionalWrittenBytes(int value) { + int wouldBeWritten = 0; + while (true) { + if ((value & ~0x7FL) == 0) { + wouldBeWritten++; + return wouldBeWritten; + } else { + wouldBeWritten++; + value >>>= 7; + } + } + } +}