Provide encode buffer hint

This commit is contained in:
Andrew Steinborn
2025-10-18 17:36:39 -04:00
parent 498a38cf74
commit d2c13c2a4c
25 changed files with 145 additions and 33 deletions

View File

@@ -32,13 +32,18 @@ public interface MinecraftPacket {
boolean handle(MinecraftSessionHandler handler);
default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
default int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return -1;
}
default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
default int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}
default int encodeSizeHint(ProtocolUtils.Direction direction,
ProtocolVersion version) {
return -1;
}
}

View File

@@ -292,6 +292,17 @@ public enum ProtocolUtils {
return str;
}
/**
* Determines the size of the written {@code str} if encoded as a VarInt-prefixed UTF-8 string.
*
* @param str the string to write
* @return the encoded size
*/
public static int stringSizeHint(CharSequence str) {
int size = ByteBufUtil.utf8Bytes(str);
return varIntBytes(size) + size;
}
/**
* Writes the specified {@code str} to the {@code buf} with a VarInt prefix.
*

View File

@@ -96,8 +96,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
}
private void doLengthSanityChecks(ByteBuf buf, MinecraftPacket packet) throws Exception {
int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version);
int expectedMinLen = packet.decodeExpectedMinLength(buf, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(buf, direction, registry.version);
if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
}

View File

@@ -54,6 +54,19 @@ public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
msg.encode(out, direction, registry.version);
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, MinecraftPacket msg,
boolean preferDirect) throws Exception {
int hint = msg.encodeSizeHint(direction, registry.version);
if (hint < 0) {
return super.allocateBuffer(ctx, msg, preferDirect);
}
int packetId = this.registry.getPacketId(msg);
int totalHint = ProtocolUtils.varIntBytes(packetId) + hint;
return preferDirect ? ctx.alloc().ioBuffer(totalHint) : ctx.alloc().heapBuffer(totalHint);
}
public void setProtocolVersion(final ProtocolVersion protocolVersion) {
this.registry = state.getProtocolRegistry(direction, protocolVersion);
}

View File

@@ -131,8 +131,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
// the packet if needed, so, we'll take advantage of the existing methods
int expectedMinLen = packet.expectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version);
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
}

View File

@@ -362,4 +362,12 @@ public class AvailableCommandsPacket implements MinecraftPacket {
return builder.buildFuture();
}
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// This is a very complex packet to encode. Paper 1.21.10 + Velocity with Spark has a size of
// 30,334, but this is likely on the lower side. We'll use 128KiB as a more realistically-sized
// amount.
return 128 * 1024;
}
}

View File

@@ -107,7 +107,7 @@ public class EncryptionResponsePacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
// It turns out these come out to the same length, whether we're talking >=1.8 or not.
// The length prefix always winds up being 2 bytes.
int base = 256 + 2 + 2;
@@ -123,8 +123,8 @@ public class EncryptionResponsePacket implements MinecraftPacket {
}
@Override
public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
int base = expectedMaxLength(buf, direction, version);
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
int base = decodeExpectedMaxLength(buf, direction, version);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
// These are "optional"
base -= 128 + 8;

View File

@@ -24,6 +24,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
public class HandshakePacket implements MinecraftPacket {
@@ -108,14 +109,21 @@ public class HandshakePacket implements MinecraftPacket {
}
@Override
public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 7;
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// We could compute an exact size, but 4KiB ought to be enough to encode all reasonable
// sizes of this packet.
return 4 * 1024;
}
}

View File

@@ -36,7 +36,7 @@ public class LoginAcknowledgedPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}

View File

@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -86,4 +87,9 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -88,4 +89,9 @@ public class LoginPluginResponsePacket extends DeferredByteBufHolder implements
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -143,4 +144,9 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
public PluginMessagePacket touch(Object hint) {
return (PluginMessagePacket) super.touch(hint);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -22,6 +22,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.Nullable;
@@ -121,4 +122,9 @@ public class ServerDataPacket implements MinecraftPacket {
public void setSecureChatEnforced(boolean secureChatEnforced) {
this.secureChatEnforced = secureChatEnforced;
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return 8 * 1024;
}
}

View File

@@ -150,7 +150,7 @@ public class ServerLoginPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
// Accommodate the rare (but likely malicious) use of UTF-8 usernames, since it is technically
// legal on the protocol level.
int base = 1 + (16 * 3);

View File

@@ -23,6 +23,7 @@ import com.velocitypowered.api.util.UuidUtils;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.util.VelocityProperties;
import io.netty.buffer.ByteBuf;
import java.util.List;
@@ -132,4 +133,11 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// We could compute an exact size, but 4KiB ought to be enough to encode all reasonable
// sizes of this packet.
return 4 * 1024;
}
}

View File

@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
@@ -45,4 +46,8 @@ public class ServerboundCustomClickActionPacket extends DeferredByteBufHolder im
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -44,12 +44,12 @@ public class StatusPingPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 8;
}
@Override
public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 8;
}
}

View File

@@ -53,7 +53,7 @@ public class StatusRequestPacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 0;
}
}

View File

@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -66,4 +67,9 @@ public class StatusResponsePacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return ProtocolUtils.stringSizeHint(this.status);
}
}

View File

@@ -36,6 +36,8 @@ import org.jetbrains.annotations.Nullable;
public class UpsertPlayerInfoPacket implements MinecraftPacket {
private static final Action[] ALL_ACTIONS = Action.class.getEnumConstants();
private final EnumSet<Action> actions;
private final List<Entry> entries;
@@ -85,14 +87,13 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket {
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) {
Action[] actions = Action.class.getEnumConstants();
byte[] bytes = new byte[-Math.floorDiv(-actions.length, 8)];
byte[] bytes = new byte[-Math.floorDiv(-ALL_ACTIONS.length, 8)];
buf.readBytes(bytes);
BitSet actionSet = BitSet.valueOf(bytes);
for (int idx = 0; idx < actions.length; idx++) {
for (int idx = 0; idx < ALL_ACTIONS.length; idx++) {
if (actionSet.get(idx)) {
addAction(actions[idx]);
addAction(ALL_ACTIONS[idx]);
}
}
@@ -109,14 +110,13 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket {
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) {
Action[] actions = Action.class.getEnumConstants();
BitSet set = new BitSet(actions.length);
for (int idx = 0; idx < actions.length; idx++) {
set.set(idx, this.actions.contains(actions[idx]));
BitSet set = new BitSet(ALL_ACTIONS.length);
for (int idx = 0; idx < ALL_ACTIONS.length; idx++) {
set.set(idx, this.actions.contains(ALL_ACTIONS[idx]));
}
byte[] bytes = set.toByteArray();
buf.writeBytes(Arrays.copyOf(bytes, -Math.floorDiv(-actions.length, 8)));
buf.writeBytes(Arrays.copyOf(bytes, -Math.floorDiv(-ALL_ACTIONS.length, 8)));
ProtocolUtils.writeVarInt(buf, this.entries.size());
for (Entry entry : this.entries) {
@@ -133,12 +133,6 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket {
return handler.handle(this);
}
public BitSet readFixedBitSet(ByteBuf buf, int param0) {
byte[] var0 = new byte[-Math.floorDiv(-param0, 8)];
buf.readBytes(var0);
return BitSet.valueOf(var0);
}
public enum Action {
ADD_PLAYER((ignored, buf, info) -> { // read
info.profile = new GameProfile(

View File

@@ -44,4 +44,9 @@ public class CodeOfConductPacket extends DeferredByteBufHolder implements Minecr
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -40,7 +40,7 @@ public class FinishedUpdatePacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}

View File

@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
@@ -47,4 +48,9 @@ public class RegistrySyncPacket extends DeferredByteBufHolder implements Minecra
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
}

View File

@@ -40,7 +40,7 @@ public class StartUpdatePacket implements MinecraftPacket {
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 0;
}

View File

@@ -22,6 +22,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
import java.util.Map;
@@ -79,4 +80,22 @@ public class TagsUpdatePacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
int size = ProtocolUtils.varIntBytes(tags.size());
for (Map.Entry<String, Map<String, int[]>> entry : tags.entrySet()) {
size += ProtocolUtils.stringSizeHint(entry.getKey());
size += ProtocolUtils.varIntBytes(entry.getValue().size());
for (Map.Entry<String, int[]> innerEntry : entry.getValue().entrySet()) {
size += ProtocolUtils.stringSizeHint(innerEntry.getKey());
size += ProtocolUtils.varIntBytes(innerEntry.getValue().length);
for (int innerEntryValue : innerEntry.getValue()) {
size += ProtocolUtils.varIntBytes(innerEntryValue);
}
}
}
return size;
}
}