From f10af6ae69297aaa4a176b80cc4a288f7f30fe95 Mon Sep 17 00:00:00 2001 From: DmitriyMX Date: Sun, 25 Apr 2021 19:01:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 15 +- .../gradle/extention/LibsExtention.java | 14 +- .../java/mc/protocol/NetworkAttributes.java | 10 + src/main/java/mc/protocol/State.java | 69 ++++ .../java/mc/protocol/io/DecoderException.java | 8 + src/main/java/mc/protocol/io/NetByteBuf.java | 183 +++++++++++ .../mc/protocol/io/codec/ProtocolDecoder.java | 59 ++++ .../mc/protocol/io/codec/ProtocolEncoder.java | 30 ++ .../protocol/io/codec/ProtocolSplitter.java | 40 +++ .../java/mc/protocol/packets/EmptyPacket.java | 16 + src/main/java/mc/protocol/packets/Packet.java | 24 ++ .../mc/protocol/packets/PacketDirection.java | 6 + .../mc/protocol/packets/UnknownPacket.java | 27 ++ .../packets/client/HandshakePacket.java | 57 ++++ .../packets/client/StatusServerRequest.java | 18 + .../packets/server/StatusServerResponse.java | 62 ++++ src/main/java/mc/server/Main.java | 2 +- src/main/java/mc/server/di/NetworkModule.java | 58 ++-- src/main/java/mc/server/network/Server.java | 2 +- .../network/netty/AbstractPacketHandler.java | 17 + .../network/netty/HandshakeHandler.java | 24 ++ .../mc/server/network/netty/NettyServer.java | 5 +- .../server/network/netty/StatusHandler.java | 33 ++ src/main/resources/logback.xml | 6 +- .../mc/protocol/io/NetByteBufReadTest.java | 307 ++++++++++++++++++ .../mc/protocol/io/NetByteBufWriteTest.java | 246 ++++++++++++++ 26 files changed, 1303 insertions(+), 35 deletions(-) create mode 100644 src/main/java/mc/protocol/NetworkAttributes.java create mode 100644 src/main/java/mc/protocol/State.java create mode 100644 src/main/java/mc/protocol/io/DecoderException.java create mode 100644 src/main/java/mc/protocol/io/NetByteBuf.java create mode 100644 src/main/java/mc/protocol/io/codec/ProtocolDecoder.java create mode 100644 src/main/java/mc/protocol/io/codec/ProtocolEncoder.java create mode 100644 src/main/java/mc/protocol/io/codec/ProtocolSplitter.java create mode 100644 src/main/java/mc/protocol/packets/EmptyPacket.java create mode 100644 src/main/java/mc/protocol/packets/Packet.java create mode 100644 src/main/java/mc/protocol/packets/PacketDirection.java create mode 100644 src/main/java/mc/protocol/packets/UnknownPacket.java create mode 100644 src/main/java/mc/protocol/packets/client/HandshakePacket.java create mode 100644 src/main/java/mc/protocol/packets/client/StatusServerRequest.java create mode 100644 src/main/java/mc/protocol/packets/server/StatusServerResponse.java create mode 100644 src/main/java/mc/server/network/netty/AbstractPacketHandler.java create mode 100644 src/main/java/mc/server/network/netty/HandshakeHandler.java create mode 100644 src/main/java/mc/server/network/netty/StatusHandler.java create mode 100644 src/test/java/mc/protocol/io/NetByteBufReadTest.java create mode 100644 src/test/java/mc/protocol/io/NetByteBufWriteTest.java diff --git a/build.gradle b/build.gradle index 7e6b85d..7de1955 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,19 @@ dependencies { implementation 'io.projectreactor:reactor-core' implementation 'io.netty:netty-all:4.1.22.Final' + implementation libs.guava + + testImplementation libs.junit5.api + testImplementation libs.junit5.params + testRuntimeOnly libs.junit5.engine + + testImplementation libs.lang3 } -application.mainClassName = 'mc.server.Main' \ No newline at end of file +application { + mainClassName = 'mc.server.Main' +} + +test { + useJUnitPlatform() +} diff --git a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java b/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java index 752fe92..05123ee 100644 --- a/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java +++ b/buildSrc/src/main/java/ru/dmitriymx/gradle/extention/LibsExtention.java @@ -6,8 +6,12 @@ public class LibsExtention { public final String lombok = "org.projectlombok:lombok:1.18.12"; public final String annotations = "com.google.code.findbugs:jsr305:3.0.2"; + public final String guava = "com.google.guava:guava:30.1-jre"; + public final String lang3 = "org.apache.commons:commons-lang3:3.11"; + public final LoggerLibs logger = new LoggerLibs(); public final Dagger2Libs dagger2 = new Dagger2Libs(); + public final Junit5Libs junit5 = new Junit5Libs(); public static final class LoggerLibs { private final String slf4j_version = "1.7.30"; @@ -17,7 +21,6 @@ public class LibsExtention { "org.slf4j:slf4j-api:" + slf4j_version, "org.slf4j:jcl-over-slf4j:" + slf4j_version ); - public final String slf4j_simple = "org.slf4j:slf4j-simple:" + slf4j_version; public final List logback = List.of( "ch.qos.logback:logback-core:" + logback_version, @@ -31,4 +34,13 @@ public class LibsExtention { public final String implementation = "com.google.dagger:dagger:" + dagger2_version; public final String annotationProcessor = "com.google.dagger:dagger-compiler:" + dagger2_version; } + + public static final class Junit5Libs { + private final String junit_version = "5.5.2"; + + public final String api = "org.junit.jupiter:junit-jupiter-api:" + junit_version; + /** runtimeOnly */ + public final String engine = "org.junit.jupiter:junit-jupiter-engine:" + junit_version; + public final String params = "org.junit.jupiter:junit-jupiter-params:" + junit_version; + } } diff --git a/src/main/java/mc/protocol/NetworkAttributes.java b/src/main/java/mc/protocol/NetworkAttributes.java new file mode 100644 index 0000000..0be033c --- /dev/null +++ b/src/main/java/mc/protocol/NetworkAttributes.java @@ -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 = AttributeKey.newInstance("STATE"); +} diff --git a/src/main/java/mc/protocol/State.java b/src/main/java/mc/protocol/State.java new file mode 100644 index 0000000..da448e6 --- /dev/null +++ b/src/main/java/mc/protocol/State.java @@ -0,0 +1,69 @@ +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.StatusServerRequest; +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) + ); + + @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> serverBoundPackets; + private final BiMap> clientBoundPackets; + + State(int id, BiMap> serverBoundPackets) { + this.id = id; + this.serverBoundPackets = serverBoundPackets; + this.clientBoundPackets = ImmutableBiMap.of(); + } + + @Nullable + public Class 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 clazz) { + if (PacketDirection.CLIENT_BOUND == direction) { + return clientBoundPackets == null ? null : clientBoundPackets.inverse().get(clazz); + } else { + return serverBoundPackets == null ? null : serverBoundPackets.inverse().get(clazz); + } + } +} diff --git a/src/main/java/mc/protocol/io/DecoderException.java b/src/main/java/mc/protocol/io/DecoderException.java new file mode 100644 index 0000000..a30ee19 --- /dev/null +++ b/src/main/java/mc/protocol/io/DecoderException.java @@ -0,0 +1,8 @@ +package mc.protocol.io; + +public class DecoderException extends RuntimeException { + + public DecoderException(String message) { + super(message); + } +} diff --git a/src/main/java/mc/protocol/io/NetByteBuf.java b/src/main/java/mc/protocol/io/NetByteBuf.java new file mode 100644 index 0000000..d1c3638 --- /dev/null +++ b/src/main/java/mc/protocol/io/NetByteBuf.java @@ -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; + +/** + * Компонент чтения и записи данных протокола. + * + *

Data types

+ *
+ * | 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] - Single-precision floating-point format
+ * [2] - Double-precision floating-point format
+ * [3] - Unicode Scalar Value
+ * 
+ * + * @see Data types + */ +@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 +} diff --git a/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java b/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java new file mode 100644 index 0000000..a59f5a0 --- /dev/null +++ b/src/main/java/mc/protocol/io/codec/ProtocolDecoder.java @@ -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 out) throws Exception { + State state = Objects.requireNonNull(ctx.channel().attr(NetworkAttributes.STATE).get()); + NetByteBuf netByteBuf = new NetByteBuf(in); + + int packetId = netByteBuf.readVarInt(); + Class 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); + } + } +} diff --git a/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java b/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java new file mode 100644 index 0000000..d26f26c --- /dev/null +++ b/src/main/java/mc/protocol/io/codec/ProtocolEncoder.java @@ -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 { + + @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); + } +} diff --git a/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java b/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java new file mode 100644 index 0000000..452bb06 --- /dev/null +++ b/src/main/java/mc/protocol/io/codec/ProtocolSplitter.java @@ -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 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); + } +} diff --git a/src/main/java/mc/protocol/packets/EmptyPacket.java b/src/main/java/mc/protocol/packets/EmptyPacket.java new file mode 100644 index 0000000..b6dd02f --- /dev/null +++ b/src/main/java/mc/protocol/packets/EmptyPacket.java @@ -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 + } +} diff --git a/src/main/java/mc/protocol/packets/Packet.java b/src/main/java/mc/protocol/packets/Packet.java new file mode 100644 index 0000000..95e1385 --- /dev/null +++ b/src/main/java/mc/protocol/packets/Packet.java @@ -0,0 +1,24 @@ +package mc.protocol.packets; + +import mc.protocol.io.NetByteBuf; + +/** + * Пакет. + * + *

Структура пакета

+ *
+ * | FIELD       | TYPE   | NOTES                                     |
+ * |-------------|--------|-------------------------------------------|
+ * | SIZE        | VarInt | = sizeOf(PACKET ID) + sizeOf(PACKET DATA) |
+ * | PACKET ID   | VarInt |                                           |
+ * | PACKET DATA | bytes  |                                           |
+ * 
+ * + * @see Packet without compression + */ +public interface Packet { + + void readSelf(NetByteBuf netByteBuf); + + void writeSelf(NetByteBuf netByteBuf); +} diff --git a/src/main/java/mc/protocol/packets/PacketDirection.java b/src/main/java/mc/protocol/packets/PacketDirection.java new file mode 100644 index 0000000..2760689 --- /dev/null +++ b/src/main/java/mc/protocol/packets/PacketDirection.java @@ -0,0 +1,6 @@ +package mc.protocol.packets; + +public enum PacketDirection { + + SERVER_BOUND, CLIENT_BOUND +} diff --git a/src/main/java/mc/protocol/packets/UnknownPacket.java b/src/main/java/mc/protocol/packets/UnknownPacket.java new file mode 100644 index 0000000..04b4873 --- /dev/null +++ b/src/main/java/mc/protocol/packets/UnknownPacket.java @@ -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); + } +} diff --git a/src/main/java/mc/protocol/packets/client/HandshakePacket.java b/src/main/java/mc/protocol/packets/client/HandshakePacket.java new file mode 100644 index 0000000..eb786bc --- /dev/null +++ b/src/main/java/mc/protocol/packets/client/HandshakePacket.java @@ -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. + * + *

Данный пакет заставляет сервер переключить текущий {@link State}

+ * + *

Структура пакета

+ *
+ * | FIELD            | TYPE           | NOTES                                        |
+ * |------------------|----------------|----------------------------------------------|
+ * | Protocol version | VarInt         | Версия протокола [1]                         |
+ * | Server address   | Stirng         | Hostname или IP                              |
+ * | Server port      | Unsigned Short | Порт сервера                                 |
+ * | Next stage       | VarInt         | ID State на который необходимо переключиться |
+ *
+ * [1] - Protocol version numbers
+ * 
+ * + * @see Handshake + * @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()); + } +} diff --git a/src/main/java/mc/protocol/packets/client/StatusServerRequest.java b/src/main/java/mc/protocol/packets/client/StatusServerRequest.java new file mode 100644 index 0000000..9cd1e61 --- /dev/null +++ b/src/main/java/mc/protocol/packets/client/StatusServerRequest.java @@ -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. + * + *

Клиент запрашивает получение информации о сервере

+ */ +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@ToString +public class StatusServerRequest extends EmptyPacket { + +} diff --git a/src/main/java/mc/protocol/packets/server/StatusServerResponse.java b/src/main/java/mc/protocol/packets/server/StatusServerResponse.java new file mode 100644 index 0000000..8cf99f4 --- /dev/null +++ b/src/main/java/mc/protocol/packets/server/StatusServerResponse.java @@ -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. + * + *

Информация о сервере

+ * + *

Структура пакета + *

+ * | FIELD         | TYPE   | NOTES                                   |
+ * |---------------|--------|-----------------------------------------|
+ * | JSON Response | String | Информация о сервере в JSON формате [1] |
+ *
+ * [1] - Server List Ping: Response
+ * 

+ */ +@Data +public class StatusServerResponse implements Packet { + + /** + * Информация о серере в формате JSON + * + *

Пример

+ *
+	 * {
+	 *     "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,<data>"
+	 * }
+	 * 
+ */ + private String info; + + @Override + public void readSelf(NetByteBuf netByteBuf) { + info = netByteBuf.readString(); + } + + @Override + public void writeSelf(NetByteBuf netByteBuf) { + netByteBuf.writeString(info); + } +} diff --git a/src/main/java/mc/server/Main.java b/src/main/java/mc/server/Main.java index bd5f9f7..a435547 100644 --- a/src/main/java/mc/server/Main.java +++ b/src/main/java/mc/server/Main.java @@ -13,6 +13,6 @@ public class Main { NetworkComponent networkComponent = DaggerNetworkComponent.create(); Server server = networkComponent.getServer(); - server.start("127.0.0.1", 25565); + server.bind("127.0.0.1", 25565); } } diff --git a/src/main/java/mc/server/di/NetworkModule.java b/src/main/java/mc/server/di/NetworkModule.java index aae8cb4..c06bb2c 100644 --- a/src/main/java/mc/server/di/NetworkModule.java +++ b/src/main/java/mc/server/di/NetworkModule.java @@ -6,19 +6,26 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; -import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.io.codec.ProtocolDecoder; +import mc.protocol.io.codec.ProtocolEncoder; +import mc.protocol.io.codec.ProtocolSplitter; import mc.server.network.Server; +import mc.server.network.netty.HandshakeHandler; import mc.server.network.netty.NettyServer; +import mc.server.network.netty.StatusHandler; -import javax.inject.Named; -import java.util.Collections; -import java.util.List; +import javax.inject.Provider; +import java.util.LinkedHashMap; +import java.util.Map; @Module +@Slf4j public class NetworkModule { @Provides @@ -27,14 +34,10 @@ public class NetworkModule { } @Provides - ServerBootstrap provideServerBootstrap( - @Named("boss-group") EventLoopGroup bossGroup, - @Named("worker-group") EventLoopGroup workerGroup, - ChannelInitializer channelChannelInitializer - ) { + ServerBootstrap provideServerBootstrap(ChannelInitializer channelChannelInitializer) { ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(bossGroup, workerGroup) + bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) .childHandler(channelChannelInitializer); @@ -42,30 +45,31 @@ public class NetworkModule { } @Provides - @Named("boss-group") - EventLoopGroup provideBossGroup() { - return new NioEventLoopGroup(1); - } - - @Provides - @Named("worker-group") - EventLoopGroup provideWorkerGroup() { - return new NioEventLoopGroup(); - } - - @Provides - ChannelInitializer provideChannelChannelInitializer(List channelHandlerList) { + ChannelInitializer provideChannelChannelInitializer(Provider> channelHandlerMapProvider) { return new ChannelInitializer<>() { @Override protected void initChannel(SocketChannel socketChannel) { - final ChannelPipeline pipeline = socketChannel.pipeline(); - channelHandlerList.forEach(pipeline::addLast); + ChannelPipeline pipeline = socketChannel.pipeline(); + channelHandlerMapProvider.get().forEach(pipeline::addLast); } }; } @Provides - List provideChannelHandlerList() { - return Collections.singletonList(new LoggingHandler()); + Map provideChannelHandlerMap(Provider statusHandlerProvider) { + Map map = new LinkedHashMap<>(); + + map.put("logger", new LoggingHandler(LogLevel.DEBUG)); + map.put("protocol_splitter", new ProtocolSplitter()); + map.put("protocol_decoder", new ProtocolDecoder(true)); + map.put("protocol_encoder", new ProtocolEncoder()); + map.put("handshake_handler", new HandshakeHandler(statusHandlerProvider)); + + return map; + } + + @Provides + StatusHandler provideStatusHandler() { + return new StatusHandler(); } } diff --git a/src/main/java/mc/server/network/Server.java b/src/main/java/mc/server/network/Server.java index f1464f8..b346a17 100644 --- a/src/main/java/mc/server/network/Server.java +++ b/src/main/java/mc/server/network/Server.java @@ -2,5 +2,5 @@ package mc.server.network; public interface Server { - void start(String host, int port); + void bind(String host, int port); } diff --git a/src/main/java/mc/server/network/netty/AbstractPacketHandler.java b/src/main/java/mc/server/network/netty/AbstractPacketHandler.java new file mode 100644 index 0000000..733e255 --- /dev/null +++ b/src/main/java/mc/server/network/netty/AbstractPacketHandler.java @@ -0,0 +1,17 @@ +package mc.server.network.netty; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import mc.protocol.packets.Packet; + +public abstract class AbstractPacketHandler

extends SimpleChannelInboundHandler { + + @SuppressWarnings("unchecked") + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet msg) throws Exception { + channelRead1(ctx, (P) msg); + } + + @SuppressWarnings("java:S112") + protected abstract void channelRead1(ChannelHandlerContext ctx, P packet) throws Exception; +} diff --git a/src/main/java/mc/server/network/netty/HandshakeHandler.java b/src/main/java/mc/server/network/netty/HandshakeHandler.java new file mode 100644 index 0000000..8ceb9a1 --- /dev/null +++ b/src/main/java/mc/server/network/netty/HandshakeHandler.java @@ -0,0 +1,24 @@ +package mc.server.network.netty; + +import io.netty.channel.ChannelHandlerContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.NetworkAttributes; +import mc.protocol.packets.client.HandshakePacket; + +import javax.inject.Provider; + +@Slf4j +@RequiredArgsConstructor +public class HandshakeHandler extends AbstractPacketHandler { + + private final Provider statusHandlerProvider; + + @Override + protected void channelRead1(ChannelHandlerContext ctx, HandshakePacket packet) { + log.info("{}", packet); + + ctx.channel().attr(NetworkAttributes.STATE).set(packet.getNextState()); + ctx.pipeline().replace("handshake_handler", "status_handler", statusHandlerProvider.get()); + } +} diff --git a/src/main/java/mc/server/network/netty/NettyServer.java b/src/main/java/mc/server/network/netty/NettyServer.java index 4c3d19e..881914c 100644 --- a/src/main/java/mc/server/network/netty/NettyServer.java +++ b/src/main/java/mc/server/network/netty/NettyServer.java @@ -12,12 +12,11 @@ public class NettyServer implements Server { private final ServerBootstrap serverBootstrap; @Override - public void start(String host, int port) { + public void bind(String host, int port) { log.info("Network starting: {}:{}", host, port); try { - serverBootstrap.bind(host, port) - .sync().channel().closeFuture().sync(); + serverBootstrap.bind(host, port).sync().channel().closeFuture().sync(); } catch (InterruptedException e) { if (log.isTraceEnabled()) { log.trace("{}: {}", e.getClass().getSimpleName(), e.getMessage(), e); diff --git a/src/main/java/mc/server/network/netty/StatusHandler.java b/src/main/java/mc/server/network/netty/StatusHandler.java new file mode 100644 index 0000000..2d898ff --- /dev/null +++ b/src/main/java/mc/server/network/netty/StatusHandler.java @@ -0,0 +1,33 @@ +package mc.server.network.netty; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.packets.client.StatusServerRequest; +import mc.protocol.packets.server.StatusServerResponse; + +@Slf4j +public class StatusHandler extends AbstractPacketHandler { + + @Override + protected void channelRead1(ChannelHandlerContext ctx, StatusServerRequest packet) { + log.info("{}", packet); + + StatusServerResponse response = new StatusServerResponse(); + response.setInfo("{\n" + + " \"version\": {\n" + + " \"name\": \"1.12.2\",\n" + + " \"protocol\": 340\n" + + " },\n" + + " \"players\": {\n" + + " \"max\": 0,\n" + + " \"online\": 0,\n" + + " \"sample\": []\n" + + " },\n" + + " \"description\": {\n" + + " \"text\": \"Hello world\"\n" + + " }\n" + + "}"); + + ctx.channel().writeAndFlush(response).channel().disconnect(); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 9f1a1a5..192c634 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,11 +4,15 @@ - %d{HH:mm:ss.SSS} %-5level [%t] [%logger{36}] -- %msg%n + %d{HH:mm:ss.SSS} %-5level [%35.35logger{34}] -- %msg%n + + + + \ No newline at end of file diff --git a/src/test/java/mc/protocol/io/NetByteBufReadTest.java b/src/test/java/mc/protocol/io/NetByteBufReadTest.java new file mode 100644 index 0000000..c48b6b4 --- /dev/null +++ b/src/test/java/mc/protocol/io/NetByteBufReadTest.java @@ -0,0 +1,307 @@ +package mc.protocol.io; + +import io.netty.buffer.ByteBuf; +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 paramsReadBoolean() { + return Stream.of( + Arguments.of((byte) 0x00, false), + Arguments.of((byte) 0x01, true) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadUnsignedByte() { + return Stream.of( + Arguments.of((byte) 30, 30), + Arguments.of((byte) (0xFF & 130), 130) + ); + } + + @SuppressWarnings("unused") + private static Stream paramsReadString() { + return Stream.of( + Arguments.of(""), + Arguments.of("Latin"), + Arguments.of("Кириллица"), + Arguments.of("العربية"), + Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления + Arguments.of("\uD800\uDD07") // Эгейские цифры, [один] + ); + } + + @SuppressWarnings("unused") + private static Stream 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 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) + ); + } +} \ No newline at end of file diff --git a/src/test/java/mc/protocol/io/NetByteBufWriteTest.java b/src/test/java/mc/protocol/io/NetByteBufWriteTest.java new file mode 100644 index 0000000..74b6c5a --- /dev/null +++ b/src/test/java/mc/protocol/io/NetByteBufWriteTest.java @@ -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 paramsWriteBoolean() { + return Stream.of( + Arguments.of(false, (byte) 0x00), + Arguments.of(true, (byte) 0x01) + ); + } + + @SuppressWarnings("unused") + private static Stream 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 paramsWriteString() { + return Stream.of( + Arguments.of(""), + Arguments.of("Latin"), + Arguments.of("Кириллица"), + Arguments.of("العربية"), + Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления + Arguments.of("\uD800\uDD07") // Эгейские цифры, [один] + ); + } + + @SuppressWarnings("unused") + private static Stream 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 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 }) + ); + } +}