Archived
0

разделение mc.protocol и mc.server на подмодули

This commit is contained in:
2021-04-26 15:27:59 +03:00
parent 38091e8685
commit fcfcb16e6d
25 changed files with 58 additions and 29 deletions

View File

@@ -0,0 +1,10 @@
package mc.protocol;
import io.netty.util.AttributeKey;
import lombok.experimental.UtilityClass;
@UtilityClass
public class NetworkAttributes {
public static final AttributeKey<State> STATE = AttributeKey.newInstance("STATE");
}

View File

@@ -0,0 +1,77 @@
package mc.protocol;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import mc.protocol.packets.Packet;
import mc.protocol.packets.PacketDirection;
import mc.protocol.packets.client.HandshakePacket;
import mc.protocol.packets.client.LoginStartPacket;
import mc.protocol.packets.client.StatusServerRequest;
import mc.protocol.packets.server.DisconnectPacket;
import mc.protocol.packets.server.StatusServerResponse;
import javax.annotation.Nullable;
@RequiredArgsConstructor
public enum State {
HANDSHAKING(-1,
// server bound
ImmutableBiMap.of(0x00, HandshakePacket.class)
),
STATUS(1,
// server bound
ImmutableBiMap.of(0x00, StatusServerRequest.class),
// client bound
ImmutableBiMap.of(0x00, StatusServerResponse.class)
),
LOGIN(2,
// server bound
ImmutableBiMap.of(0x00, LoginStartPacket.class),
// client bound
ImmutableBiMap.of(0x00, DisconnectPacket.class)
);
@Nullable
public static State getById(int id) {
for (State state : State.values()) {
if (state.id == id) {
return state;
}
}
return null;
}
@Getter
private final int id;
private final BiMap<Integer, Class<? extends Packet>> serverBoundPackets;
private final BiMap<Integer, Class<? extends Packet>> clientBoundPackets;
State(int id, BiMap<Integer, Class<? extends Packet>> serverBoundPackets) {
this.id = id;
this.serverBoundPackets = serverBoundPackets;
this.clientBoundPackets = ImmutableBiMap.of();
}
@Nullable
public Class<? extends Packet> getPacketById(PacketDirection direction, int id) {
if (PacketDirection.CLIENT_BOUND == direction) {
return clientBoundPackets == null ? null : clientBoundPackets.get(id);
} else {
return serverBoundPackets == null ? null : serverBoundPackets.get(id);
}
}
@Nullable
public Integer getIdByPacket(PacketDirection direction, Class<? extends Packet> clazz) {
if (PacketDirection.CLIENT_BOUND == direction) {
return clientBoundPackets == null ? null : clientBoundPackets.inverse().get(clazz);
} else {
return serverBoundPackets == null ? null : serverBoundPackets.inverse().get(clazz);
}
}
}

View File

@@ -0,0 +1,8 @@
package mc.protocol.io;
public class DecoderException extends RuntimeException {
public DecoderException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,183 @@
package mc.protocol.io;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* Компонент чтения и записи данных протокола.
*
* <p>Data types</p>
* <pre>
* | TYPE | SIZE (bytes) | ENCODING | NOTES |
* |----------------|-----------------------|-----------------------------------------------------|--------------------------------------------------------------------------|
* | Boolean | 1 | True или False | True = 0x01; False = 0x00 |
* | Byte | 1 | Число от -128 до 127 | 8-bit число со знаком |
* | Unsigned Byte | 1 | Число от 0 до 255 | 8-bit без знаковое число |
* | Short | 2 | Число от -32768 до 32767 | 16-bit число со знаком |
* | Unsigned Short | 2 | Число от -32768 до 32767 | 16-bit без знаковое число |
* | Int | 4 | Число от -2147483648 и 2147483647 | 32-bit число со знаком |
* | Long | 8 | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число со знаком |
* | Float | 4 | 32-bit число одинарной точности (IEEE 754-2008) | [1] |
* | Double | 8 | 64-bit число одинарной точности (IEEE 754-2008) | [2] |
* | String (n) | >= 1 ; <= (n * 4) + 3 | Последовательность Unicode scalar values | В начале пишется длина строки в VarInt, после чего записываются символы. |
* | | | | Каждый символ может состоять максимум из 4 байт. [3] |
* | | | | Максимальная длина строки - 32767 (3 - это как раз размер VarInt для |
* | | | | этого числа). |
* | VarInt | >= 1 ; <= 5 | Число от -2147483648 и 2147483647 | 32-bit число с плавающей размерностью от 1 до 5 байт |
* | VarLong | >= 1 ; <= 10 | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число с плавающей размерностью от 1 до 10 байт |
*
* [1] - <a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format">Single-precision floating-point format</a>
* [2] - <a href="https://en.wikipedia.org/wiki/Double-precision_floating-point_format">Double-precision floating-point format</a>
* [3] - <a href="http://unicode.org/glossary/#unicode_scalar_value">Unicode Scalar Value</a>
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Data_types">Data types</a>
*/
@Slf4j
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString
public class NetByteBuf extends ByteBuf {
@Delegate
private final ByteBuf byteBuf;
//region String
public String readString() {
return readString(Short.MAX_VALUE);
}
@SuppressWarnings("java:S131")
public String readString(int maxLength) {
int length = readVarInt();
if (length == 0) {
return "";
} else if (length > maxLength) {
throw new DecoderException("String length exceeds maximum length: " + length + " > " + maxLength);
} else if (length < 0) {
throw new DecoderException("String length less zero!");
}
byte[] bytes = new byte[length * 4];
int readbleBytes = 0;
for (int i = 0; i < length && readableBytes() > 0; i++) {
byte b = readByte();
bytes[readbleBytes++] = b;
switch ((b & 0xFF) >> 4) {
case 0b1100:
case 0b1101:
bytes[readbleBytes++] = readByte();
break;
case 0b1110:
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
break;
case 0b1111:
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
break;
}
}
return new String(bytes, 0, readbleBytes, StandardCharsets.UTF_8);
}
public void writeString(String string) {
byte[] buf;
int length = (int) string.codePoints().count();
if (length > Short.MAX_VALUE) {
log.warn("String is too long: {} > {}", length, Short.MAX_VALUE);
buf = string.substring(0, Short.MAX_VALUE).getBytes(StandardCharsets.UTF_8);
writeVarInt(Short.MAX_VALUE);
} else {
buf = string.getBytes(StandardCharsets.UTF_8);
writeVarInt(length);
}
writeBytes(buf);
}
//endregion
//region VarInt
public int readVarInt() {
int numRead = 0;
int result = 0;
byte read;
do {
if ((numRead + 1) > 5) {
log.warn("VarInt is too big");
break;
}
read = readByte();
int value = (read & 0b01111111);
result |= (value << (7 * numRead));
numRead++;
} while ((read & 0b10000000) != 0);
return result;
}
public void writeVarInt(int value) {
while ((value & -128) != 0) {
writeByte(value & 127 | 128);
value >>>= 7;
}
writeByte(value);
}
//endregion
//region VarLong
public long readVarLong() {
int numRead = 0;
long result = 0L;
byte read;
do {
if (numRead > 10) {
log.warn("VarLong is too big");
break;
}
read = readByte();
long value = (read & 0b01111111);
result |= (value << (7 * numRead));
numRead++;
} while ((read & 0b10000000) != 0);
return result;
}
public void writeVarLong(long value) {
while ((value & -128L) != 0L) {
writeByte((int) (value & 127L) | 128);
value >>>= 7;
}
writeByte((int) value);
}
//endregion
//region UUID
public UUID readUUID() {
return new UUID(readLong(), readLong());
}
public void writeUUID(UUID uuid) {
writeLong(uuid.getMostSignificantBits());
writeLong(uuid.getLeastSignificantBits());
}
//endregion
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.io.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.NetworkAttributes;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
import mc.protocol.packets.PacketDirection;
import mc.protocol.packets.UnknownPacket;
import java.util.List;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
public class ProtocolDecoder extends ByteToMessageDecoder {
private final boolean readUnknownPackets;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(NetworkAttributes.STATE).set(State.HANDSHAKING);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(NetworkAttributes.STATE).set(null);
super.channelInactive(ctx);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
State state = Objects.requireNonNull(ctx.channel().attr(NetworkAttributes.STATE).get());
NetByteBuf netByteBuf = new NetByteBuf(in);
int packetId = netByteBuf.readVarInt();
Class<? extends Packet> packetClass = state.getPacketById(PacketDirection.SERVER_BOUND, packetId);
if (packetClass == null) {
log.warn("Unkown packet: State {} ; Id {}", state, packetId);
if (readUnknownPackets) {
UnknownPacket unknownPacket = new UnknownPacket(state, packetId, netByteBuf.readableBytes());
unknownPacket.readSelf(netByteBuf);
out.add(unknownPacket);
} else {
netByteBuf.skipBytes(netByteBuf.readableBytes());
}
} else {
Packet packet = packetClass.getDeclaredConstructor().newInstance();
packet.readSelf(netByteBuf);
out.add(packet);
}
}
}

View File

@@ -0,0 +1,30 @@
package mc.protocol.io.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import mc.protocol.NetworkAttributes;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
import mc.protocol.packets.PacketDirection;
import java.util.Objects;
public class ProtocolEncoder extends MessageToByteEncoder<Packet> {
@Override
protected void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf out) {
State state = ctx.channel().attr(NetworkAttributes.STATE).get();
int packetId = Objects.requireNonNull(state.getIdByPacket(PacketDirection.CLIENT_BOUND, packet.getClass()));
NetByteBuf buffer = new NetByteBuf(Unpooled.buffer());
buffer.writeVarInt(packetId);
packet.writeSelf(buffer);
NetByteBuf netByteBuf = new NetByteBuf(out);
netByteBuf.writeVarInt(buffer.readableBytes());
netByteBuf.writeBytes(buffer);
}
}

View File

@@ -0,0 +1,40 @@
package mc.protocol.io.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import mc.protocol.io.NetByteBuf;
import java.util.List;
public class ProtocolSplitter extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
NetByteBuf netByteBuf = new NetByteBuf(in);
netByteBuf.markReaderIndex();
do {
byte[] sizePacketRaw = new byte[3];
for (int i = 0; i < 3; ++i) {
sizePacketRaw[i] = netByteBuf.readByte();
if (sizePacketRaw[i] >= 0) {
break;
}
}
int sizePacket = new NetByteBuf(Unpooled.wrappedBuffer(sizePacketRaw)).readVarInt();
if (netByteBuf.readableBytes() >= sizePacket) {
byte[] bytes = new byte[sizePacket];
netByteBuf.readBytes(bytes);
out.add(Unpooled.wrappedBuffer(bytes));
} else {
netByteBuf.resetReaderIndex();
break;
}
} while (netByteBuf.readableBytes() > 0);
}
}

View File

@@ -0,0 +1,16 @@
package mc.protocol.packets;
import mc.protocol.io.NetByteBuf;
public abstract class EmptyPacket implements Packet {
@Override
public void readSelf(NetByteBuf netByteBuf) {
// empty
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
// empty
}
}

View File

@@ -0,0 +1,24 @@
package mc.protocol.packets;
import mc.protocol.io.NetByteBuf;
/**
* Пакет.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|-------------------------------------------|
* | SIZE | VarInt | = sizeOf(PACKET ID) + sizeOf(PACKET DATA) |
* | PACKET ID | VarInt | |
* | PACKET DATA | bytes | |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Without_compression">Packet without compression</a>
*/
public interface Packet {
void readSelf(NetByteBuf netByteBuf);
void writeSelf(NetByteBuf netByteBuf);
}

View File

@@ -0,0 +1,6 @@
package mc.protocol.packets;
public enum PacketDirection {
SERVER_BOUND, CLIENT_BOUND
}

View File

@@ -0,0 +1,27 @@
package mc.protocol.packets;
import lombok.Data;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
@Data
@ToString(exclude = "rawData")
public class UnknownPacket implements Packet {
private final State state;
private final int id;
private final int dataSize;
private byte[] rawData;
@Override
public void readSelf(NetByteBuf netByteBuf) {
rawData = new byte[dataSize];
netByteBuf.readBytes(rawData);
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeBytes(rawData);
}
}

View File

@@ -0,0 +1,57 @@
package mc.protocol.packets.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
/**
* Handshake packet.
*
* <p>Данный пакет заставляет сервер переключить текущий {@link State}</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------------|----------------|----------------------------------------------|
* | Protocol version | VarInt | Версия протокола [1] |
* | Server address | Stirng | Hostname или IP |
* | Server port | Unsigned Short | Порт сервера |
* | Next stage | VarInt | ID State на который необходимо переключиться |
*
* [1] - <a href="https://wiki.vg/Protocol_version_numbers" target="_top">Protocol version numbers</a>
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Handshake" target="_top">Handshake</a>
* @see State
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class HandshakePacket implements Packet {
private int protocolVersion;
private String host;
private int port;
private State nextState;
@Override
public void readSelf(NetByteBuf netByteBuf) {
protocolVersion = netByteBuf.readVarInt();
host = netByteBuf.readString(255);
port = netByteBuf.readUnsignedShort();
nextState = State.getById(netByteBuf.readVarInt());
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeVarInt(protocolVersion);
netByteBuf.writeString(host);
netByteBuf.writeShort(port);
netByteBuf.writeVarInt(nextState.getId());
}
}

View File

@@ -0,0 +1,43 @@
package mc.protocol.packets.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
/**
* Login start packet.
*
* <p>Начало авторизации.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------|--------|------------------|
* | Name | String | Имя/Логин игрока |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Login_Start" target="_top">Login start</a>
* @see State
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class LoginStartPacket implements Packet {
private String name;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.name = netByteBuf.readString();
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(name);
}
}

View File

@@ -0,0 +1,18 @@
package mc.protocol.packets.client;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.packets.EmptyPacket;
/**
* Status server packet, request.
*
* <p>Клиент запрашивает получение информации о сервере</p>
*/
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString
public class StatusServerRequest extends EmptyPacket {
}

View File

@@ -0,0 +1,47 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.State;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
/**
* Diconnect packet.
*
* <p>Отключение клиента сервером с указанием причины.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------|------|----------------------------------|
* | Reason | Text | Причина отключения. Опционально. |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Disconnect_2" target="_top">Disconnect</a>
* @see State
*/
@Data
public class DisconnectPacket implements Packet {
/**
* Причина отключения.
*
* <p>Пример:</p>
* <pre>
* {
* "text": "foo"
* }
* </pre>
*/
private String reason;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.reason = netByteBuf.readString();
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(reason);
}
}

View File

@@ -0,0 +1,62 @@
package mc.protocol.packets.server;
import lombok.Data;
import mc.protocol.io.NetByteBuf;
import mc.protocol.packets.Packet;
/**
* Status server packet, response.
*
* <p>Информация о сервере</p>
*
* <p>Структура пакета
* <pre>
* | FIELD | TYPE | NOTES |
* |---------------|--------|-----------------------------------------|
* | JSON Response | String | Информация о сервере в JSON формате [1] |
*
* [1] - <a href="https://wiki.vg/index.php?title=Server_List_Ping&oldid=7555#Response" target="_top">Server List Ping: Response</a>
* </pre></p>
*/
@Data
public class StatusServerResponse implements Packet {
/**
* Информация о серере в формате JSON
*
* <p>Пример</p>
* <pre>
* {
* "version": {
* "name": "1.8.7",
* "protocol": 47
* },
* "players": {
* "max": 100,
* "online": 5,
* "sample": [
* {
* "name": "thinkofdeath",
* "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"
* }
* ]
* },
* "description": {
* "text": "Hello world"
* },
* "favicon": "data:image/png;base64,&lt;data&gt;"
* }
* </pre>
*/
private String info;
@Override
public void readSelf(NetByteBuf netByteBuf) {
info = netByteBuf.readString();
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(info);
}
}

View File

@@ -0,0 +1,306 @@
package mc.protocol.io;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class NetByteBufReadTest {
private static Random random;
private ByteArrayOutputStream baos;
@BeforeEach
void setUp() {
random = new Random(System.currentTimeMillis());
baos = new ByteArrayOutputStream();
}
@ParameterizedTest
@MethodSource("paramsReadBoolean")
void readBoolean(byte sourceByte, boolean expectedValue) {
baos.write(sourceByte);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readBoolean());
}
@Test
void readByte() {
byte[] bytes = new byte[1];
random.nextBytes(bytes);
baos.write(bytes[0]);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(bytes[0], netByteBuf.readByte());
}
@ParameterizedTest
@MethodSource("paramsReadUnsignedByte")
void readUnsignedByte(byte sourceByte, int expectedValue) {
baos.write(sourceByte);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readUnsignedByte());
}
@Test
void readShort() throws IOException {
int value = Integer.valueOf(random.nextInt()).shortValue();
new DataOutputStream(baos).writeShort(value);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(value, netByteBuf.readShort());
}
@Test
void readUnsignedShort() throws IOException {
int value = 32768;
new DataOutputStream(baos).writeShort(value);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(value, netByteBuf.readUnsignedShort());
}
@ParameterizedTest
@MethodSource("paramsReadString")
void readString(String string) throws IOException {
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 1];
bytes[0] = (byte) string.codePoints().count(); // допустим, что размер поместился в один байт
System.arraycopy(strBytes, 0, bytes, 1, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(string, netByteBuf.readString());
}
@Test
void readString_overSize() throws IOException {
String string = "123";
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 1];
final int length = string.length();
bytes[0] = (byte) (length + 1); // допустим, что размер поместился в один байт
System.arraycopy(strBytes, 0, bytes, 1, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertThrows(DecoderException.class, () -> netByteBuf.readString(length));
}
@Test
void readString_lessZero() throws IOException {
String string = "123";
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 5];
bytes[0] = (byte) 0xFF;
bytes[1] = (byte) 0xFF;
bytes[2] = (byte) 0xFF;
bytes[3] = (byte) 0xFF;
bytes[4] = (byte) 0x0F;
System.arraycopy(strBytes, 0, bytes, 5, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertThrows(DecoderException.class, () -> netByteBuf.readString(-1));
}
@ParameterizedTest
@MethodSource("paramsReadVarInt")
void readVarInt(byte[] sourceBytes, int expectedValue) throws IOException {
baos.write(sourceBytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readVarInt());
}
@Test
void readVarInt_tooBig() throws IOException {
baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F });
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(-1, netByteBuf.readVarInt());
}
@ParameterizedTest
@MethodSource({"paramsReadVarInt", "paramsReadVarLong"})
void readVarLong(byte[] sourceBytes, long expectedValue) throws IOException {
baos.write(sourceBytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readVarLong());
}
@Test
void readVarLong_tooBig() throws IOException {
baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0x0F });
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(-1, netByteBuf.readVarLong());
}
@Test
void readUUID() throws IOException {
final UUID uuid = UUID.randomUUID();
final long mostSignificantBits = uuid.getMostSignificantBits();
final long leastSignificantBits = uuid.getLeastSignificantBits();
baos.write(new byte[]{
(byte) ((mostSignificantBits >>> 56) & 0xFF),
(byte) ((mostSignificantBits >>> 48) & 0xFF),
(byte) ((mostSignificantBits >>> 40) & 0xFF),
(byte) ((mostSignificantBits >>> 32) & 0xFF),
(byte) ((mostSignificantBits >>> 24) & 0xFF),
(byte) ((mostSignificantBits >>> 16) & 0xFF),
(byte) ((mostSignificantBits >>> 8) & 0xFF),
(byte) (mostSignificantBits & 0xFF)
});
baos.write(new byte[]{
(byte) ((leastSignificantBits >>> 56) & 0xFF),
(byte) ((leastSignificantBits >>> 48) & 0xFF),
(byte) ((leastSignificantBits >>> 40) & 0xFF),
(byte) ((leastSignificantBits >>> 32) & 0xFF),
(byte) ((leastSignificantBits >>> 24) & 0xFF),
(byte) ((leastSignificantBits >>> 16) & 0xFF),
(byte) ((leastSignificantBits >>> 8) & 0xFF),
(byte) (leastSignificantBits & 0xFF)
});
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(uuid, netByteBuf.readUUID());
}
@Test
void readBytes() throws IOException {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
baos.write(bytes);
byte[] actualBytes = new byte[128];
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(bytes.length, netByteBuf.readableBytes());
netByteBuf.readBytes(actualBytes);
assertArrayEquals(bytes, actualBytes);
assertEquals(0, netByteBuf.readableBytes());
}
@Test
void read_offset() throws IOException {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
baos.write(bytes);
byte[] buff = new byte[128];
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
netByteBuf.readBytes(buff, 3, 11);
byte[] expectedBytes = new byte[11];
System.arraycopy(bytes, 0, expectedBytes, 0, 11);
byte[] actualBytes = new byte[11];
System.arraycopy(buff, 3, actualBytes, 0, 11);
assertArrayEquals(expectedBytes, actualBytes);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadBoolean() {
return Stream.of(
Arguments.of((byte) 0x00, false),
Arguments.of((byte) 0x01, true)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadUnsignedByte() {
return Stream.of(
Arguments.of((byte) 30, 30),
Arguments.of((byte) (0xFF & 130), 130)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadString() {
return Stream.of(
Arguments.of(""),
Arguments.of("Latin"),
Arguments.of("Кириллица"),
Arguments.of("العربية"),
Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления
Arguments.of("\uD800\uDD07") // Эгейские цифры, [один]
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadVarInt() {
return Stream.of(
Arguments.of(new byte[]{ 0x78 }, 120),
Arguments.of(new byte[]{ (byte) 0xE0, 0x5D }, 12000),
Arguments.of(new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }, 120000),
Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }, 120_000_000),
Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }, 1_200_000_000)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadVarLong() {
return Stream.of(
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C },
12_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 },
120_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD,
0x02 },
12_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC,
(byte) 0x90, 0x02},
1_200_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB,
(byte) 0x94, (byte) 0xD5, 0x01 },
120_000_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 },
Long.MIN_VALUE)
);
}
}

View File

@@ -0,0 +1,246 @@
package mc.protocol.io;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class NetByteBufWriteTest {
private static Random random;
@BeforeAll
static void setUp() {
random = new Random(System.currentTimeMillis());
}
@ParameterizedTest
@MethodSource("paramsWriteBoolean")
void writeBoolean(boolean sourceValue, byte expectedByte) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBoolean(sourceValue);
assertEquals(expectedByte, byteBuf.array()[0]);
}
@ParameterizedTest
@MethodSource("paramsWriteByte")
void writeByte(byte sourceValue, byte expectedByte) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeByte(sourceValue);
assertEquals(expectedByte, byteBuf.array()[0]);
}
@ParameterizedTest
@MethodSource("paramsWriteString")
void writeString(String string) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeString(string);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
int length = actualArray[0]; // допустим, что размер поместился в один байт
assertEquals(string.codePoints().count(), length);
byte[] dataBytes = new byte[actualArray.length - 1];
System.arraycopy(actualArray, 1, dataBytes, 0, dataBytes.length);
assertEquals(string, new String(dataBytes, StandardCharsets.UTF_8));
}
//возможно этот тест нужно перенести в NetByteBufReadTest
@Test
void writeString_overSize() {
String overSizeString = RandomStringUtils.randomAscii(Short.MAX_VALUE + Short.MAX_VALUE);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeString(overSizeString);
NetByteBuf netByteBuf2 = new NetByteBuf(byteBuf.copy());
String actualString = netByteBuf2.readString();
String expectedString = overSizeString.substring(0, Short.MAX_VALUE);
assertEquals(expectedString, actualString);
}
@ParameterizedTest
@MethodSource("paramsWriteVarInt")
void writeVarInt(int sourceValue, byte[] expectedBytes) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeVarInt(sourceValue);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(expectedBytes, actualArray);
}
@ParameterizedTest
@MethodSource({ "paramsWriteVarInt", "paramsWriteVarLong" })
void writeVarLong(long sourceValue, byte[] expectedBytes) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeVarLong(sourceValue);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(expectedBytes, actualArray);
}
@Test
void writeUUID() {
final UUID uuid = UUID.randomUUID();
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeUUID(uuid);
final long mostSignificantBits = uuid.getMostSignificantBits();
final long leastSignificantBits = uuid.getLeastSignificantBits();
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(new byte[]{
(byte) ((mostSignificantBits >>> 56) & 0xFF),
(byte) ((mostSignificantBits >>> 48) & 0xFF),
(byte) ((mostSignificantBits >>> 40) & 0xFF),
(byte) ((mostSignificantBits >>> 32) & 0xFF),
(byte) ((mostSignificantBits >>> 24) & 0xFF),
(byte) ((mostSignificantBits >>> 16) & 0xFF),
(byte) ((mostSignificantBits >>> 8) & 0xFF),
(byte) (mostSignificantBits & 0xFF),
(byte) ((leastSignificantBits >>> 56) & 0xFF),
(byte) ((leastSignificantBits >>> 48) & 0xFF),
(byte) ((leastSignificantBits >>> 40) & 0xFF),
(byte) ((leastSignificantBits >>> 32) & 0xFF),
(byte) ((leastSignificantBits >>> 24) & 0xFF),
(byte) ((leastSignificantBits >>> 16) & 0xFF),
(byte) ((leastSignificantBits >>> 8) & 0xFF),
(byte) (leastSignificantBits & 0xFF) },
actualArray);
}
@Test
void writeBytes() {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBytes(bytes);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(bytes, actualArray);
}
@Test
void write_offset() {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBytes(bytes, 3, 11);
byte[] actualBytes = new byte[11];
System.arraycopy(byteBuf.array(), 0, actualBytes, 0, 11);
byte[] expectedBytes = new byte[11];
System.arraycopy(bytes, 3, expectedBytes, 0, 11);
assertArrayEquals(expectedBytes, actualBytes);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteBoolean() {
return Stream.of(
Arguments.of(false, (byte) 0x00),
Arguments.of(true, (byte) 0x01)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteByte() {
byte b = Integer.valueOf(random.nextInt()).byteValue();
return Stream.of(
Arguments.of(b, b),
Arguments.of((byte) 128, (byte) -128)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteString() {
return Stream.of(
Arguments.of(""),
Arguments.of("Latin"),
Arguments.of("Кириллица"),
Arguments.of("العربية"),
Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления
Arguments.of("\uD800\uDD07") // Эгейские цифры, [один]
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteVarInt() {
return Stream.of(
Arguments.of(120, new byte[]{ 0x78 }),
Arguments.of(12000, new byte[]{ (byte) 0xE0, 0x5D }),
Arguments.of(120000, new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }),
Arguments.of(120000000, new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }),
Arguments.of(1200000000, new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 })
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteVarLong() {
return Stream.of(
Arguments.of(
12_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C }),
Arguments.of(
120_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 }),
Arguments.of(
12_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD,
0x02 }),
Arguments.of(
1_200_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC,
(byte) 0x90, 0x02 }),
Arguments.of(
120_000_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB,
(byte) 0x94, (byte) 0xD5, 0x01 }),
Arguments.of(
Long.MIN_VALUE,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 })
);
}
}