From a293ae021d624215a61345fc85ff39e06ded0a85 Mon Sep 17 00:00:00 2001 From: DmitriyMX Date: Sat, 2 May 2020 04:59:03 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20NetInputStream=20=D0=B8=20NetOutputStream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/mc/protocol/io/NetInputStream.java | 78 ++++++++++++++++--- .../java/mc/protocol/io/NetOutputStream.java | 28 ++++++- .../io/coder/ByteArrayNetOutputStream.java | 31 ++++++++ 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/src/main/java/mc/protocol/io/NetInputStream.java b/src/main/java/mc/protocol/io/NetInputStream.java index fa2169e..44950af 100644 --- a/src/main/java/mc/protocol/io/NetInputStream.java +++ b/src/main/java/mc/protocol/io/NetInputStream.java @@ -3,6 +3,7 @@ package mc.protocol.io; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.UUID; public abstract class NetInputStream extends InputStream { @@ -11,11 +12,13 @@ public abstract class NetInputStream extends InputStream { } @Override - public int read() throws IOException { + public int read() { return readByte(); } - // Unsigned Byte [1] + public int readUnsignedByte() { + return readByte() & 0xFF; + } public int readUnsignedShort() { return readShort() & 0xFFFF; @@ -39,20 +42,37 @@ public abstract class NetInputStream extends InputStream { if (length == 0) { return ""; } else if (length > maxLength) { - throw new DecoderException("String length exceeds maximum length: " + 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]; - readBytes(bytes); - return new String(bytes, StandardCharsets.UTF_8); + byte[] bytes = new byte[length * 4]; + int readbleBytes = 0; + for (int i = 0; i < length; 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); } - // Chat - - // Identifier - public int readVarInt() { int numRead = 0; int result = 0; @@ -72,7 +92,25 @@ public abstract class NetInputStream extends InputStream { return result; } - // VarLong + public long readVarLong() { + int numRead = 0; + long result = 0L; + byte read; + do { + if (numRead > 10) { + //FIXME выводить в лог предупреждение + break; // VarLong is too big + } + + read = readByte(); + long value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + } while ((read & 0b10000000) != 0); + + return result; + } // Entity Metadata @@ -84,7 +122,9 @@ public abstract class NetInputStream extends InputStream { // Angle [1] - // UUID [16] + public UUID readUUID() { + return new UUID(readLong(), readLong()); + } public int readBytes(byte[] buffer) { return readBytes(buffer, 0, buffer.length); @@ -100,9 +140,23 @@ public abstract class NetInputStream extends InputStream { return readBytes(buffer, offset, length); } + public abstract void markReadIndex(); + + public abstract void resetReadIndex(); + + public abstract int readableBytes(); + public abstract byte readByte(); public abstract int readBytes(byte[] buffer, int offset, int lengtn); public abstract int readShort(); + + public abstract int readInt(); + + public abstract long readLong(); + + public abstract float readFloat(); + + public abstract double readDouble(); } diff --git a/src/main/java/mc/protocol/io/NetOutputStream.java b/src/main/java/mc/protocol/io/NetOutputStream.java index 0435ad0..7ffb26d 100644 --- a/src/main/java/mc/protocol/io/NetOutputStream.java +++ b/src/main/java/mc/protocol/io/NetOutputStream.java @@ -3,6 +3,7 @@ package mc.protocol.io; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.UUID; public abstract class NetOutputStream extends OutputStream { @@ -29,14 +30,15 @@ public abstract class NetOutputStream extends OutputStream { public void writeString(String string) { byte[] buf; + int length = (int) string.codePoints().count(); - if (string.length() > Short.MAX_VALUE) { + if (length > Short.MAX_VALUE) { //FIXME нужно выдавать предупреждение в лог buf = string.substring(0, Short.MAX_VALUE).getBytes(StandardCharsets.UTF_8); writeVarInt(Short.MAX_VALUE); } else { buf = string.getBytes(StandardCharsets.UTF_8); - writeVarInt(string.length()); + writeVarInt(length); } writeBytes(buf); @@ -55,7 +57,14 @@ public abstract class NetOutputStream extends OutputStream { writeByte(value); } - // VarLong + public void writeVarLong(long value) { + while ((value & -128L) != 0L) { + writeByte((int)(value & 127L) | 128); + value >>>= 7; + } + + writeByte((int)value); + } // Entity Metadata @@ -67,7 +76,10 @@ public abstract class NetOutputStream extends OutputStream { // Angle [1] - // UUID [16] + public void writeUUID(UUID uuid) { + writeLong(uuid.getMostSignificantBits()); + writeLong(uuid.getLeastSignificantBits()); + } public void writeBytes(byte[] buffer) { writeBytes(buffer, 0, buffer.length); @@ -88,4 +100,12 @@ public abstract class NetOutputStream extends OutputStream { public abstract void writeBytes(byte[] buffer, int offset, int lengtn); public abstract void writeShort(int value); + + public abstract void writeInt(int value); + + public abstract void writeLong(long value); + + public abstract void writeFloat(float value); + + public abstract void writeDouble(double value); } diff --git a/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java b/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java index f42ee07..ba93d38 100644 --- a/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java +++ b/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java @@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream; class ByteArrayNetOutputStream extends NetOutputStream { + //TODO может заменить на DataOutputStream? private ByteArrayOutputStream baos = new ByteArrayOutputStream(); @Override @@ -24,6 +25,36 @@ class ByteArrayNetOutputStream extends NetOutputStream { baos.write(value & 0xFF); } + @Override + public void writeInt(int value) { + baos.write((value >>> 24) & 0xFF); + baos.write((value >>> 16) & 0xFF); + baos.write((value >>> 8) & 0xFF); + baos.write(value & 0xFF); + } + + @Override + public void writeLong(long value) { + baos.write((int) ((value >>> 56) & 0xFF)); + baos.write((int) ((value >>> 48) & 0xFF)); + baos.write((int) ((value >>> 40) & 0xFF)); + baos.write((int) ((value >>> 32) & 0xFF)); + baos.write((int) ((value >>> 24) & 0xFF)); + baos.write((int) ((value >>> 16) & 0xFF)); + baos.write((int) ((value >>> 8) & 0xFF)); + baos.write((int) (value & 0xFF)); + } + + @Override + public void writeFloat(float value) { + writeInt(Float.floatToIntBits(value)); + } + + @Override + public void writeDouble(double value) { + writeLong(Double.doubleToLongBits(value)); + } + int size() { return baos.size(); } From c67d9f9a6cf19dcdc155ee06955d0d2424cd4278 Mon Sep 17 00:00:00 2001 From: DmitriyMX Date: Sat, 2 May 2020 05:18:16 +0300 Subject: [PATCH 2/2] test: NetInputStreamTest, NetOutputStreamTest --- build.gradle | 10 + .../{coder => }/ByteArrayNetOutputStream.java | 12 +- .../mc/protocol/io/coder/ProtocolEncoder.java | 1 + .../protocol/io/ByteArrayNetInputStream.java | 63 ++++ .../mc/protocol/io/NetInputStreamTest.java | 300 ++++++++++++++++++ .../mc/protocol/io/NetOutputStreamTest.java | 230 ++++++++++++++ 6 files changed, 609 insertions(+), 7 deletions(-) rename src/main/java/mc/protocol/io/{coder => }/ByteArrayNetOutputStream.java (85%) create mode 100644 src/test/java/mc/protocol/io/ByteArrayNetInputStream.java create mode 100644 src/test/java/mc/protocol/io/NetInputStreamTest.java create mode 100644 src/test/java/mc/protocol/io/NetOutputStreamTest.java diff --git a/build.gradle b/build.gradle index 3dd56ba..42319e2 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ repositories { ext { slf4j_version = '1.7.25' lombok_version = '1.18.2' + junit_version = '5.5.2' } dependencies { @@ -29,5 +30,14 @@ dependencies { /* LOMBOK */ annotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) compileOnly (group: 'org.projectlombok', name: 'lombok', version: lombok_version) + + /* TESTING */ + testImplementation (group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junit_version) + testRuntimeOnly (group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit_version) + testImplementation (group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit_version) + testImplementation (group: 'org.apache.commons', name: 'commons-lang3', version: '3.9') } +test { + useJUnitPlatform() +} diff --git a/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java b/src/main/java/mc/protocol/io/ByteArrayNetOutputStream.java similarity index 85% rename from src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java rename to src/main/java/mc/protocol/io/ByteArrayNetOutputStream.java index ba93d38..b5192df 100644 --- a/src/main/java/mc/protocol/io/coder/ByteArrayNetOutputStream.java +++ b/src/main/java/mc/protocol/io/ByteArrayNetOutputStream.java @@ -1,13 +1,11 @@ -package mc.protocol.io.coder; - -import mc.protocol.io.NetOutputStream; +package mc.protocol.io; import java.io.ByteArrayOutputStream; -class ByteArrayNetOutputStream extends NetOutputStream { +public class ByteArrayNetOutputStream extends NetOutputStream { //TODO может заменить на DataOutputStream? - private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); @Override public void writeByte(int value) { @@ -55,11 +53,11 @@ class ByteArrayNetOutputStream extends NetOutputStream { writeLong(Double.doubleToLongBits(value)); } - int size() { + public int size() { return baos.size(); } - byte[] toByteArray() { + public byte[] toByteArray() { return baos.toByteArray(); } } diff --git a/src/main/java/mc/protocol/io/coder/ProtocolEncoder.java b/src/main/java/mc/protocol/io/coder/ProtocolEncoder.java index 973cf8d..1f04d06 100644 --- a/src/main/java/mc/protocol/io/coder/ProtocolEncoder.java +++ b/src/main/java/mc/protocol/io/coder/ProtocolEncoder.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import mc.protocol.Packet; import mc.protocol.PacketDirection; import mc.protocol.State; +import mc.protocol.io.ByteArrayNetOutputStream; import mc.protocol.io.NetOutputStream; import java.util.Objects; diff --git a/src/test/java/mc/protocol/io/ByteArrayNetInputStream.java b/src/test/java/mc/protocol/io/ByteArrayNetInputStream.java new file mode 100644 index 0000000..9214dee --- /dev/null +++ b/src/test/java/mc/protocol/io/ByteArrayNetInputStream.java @@ -0,0 +1,63 @@ +package mc.protocol.io; + +import java.nio.ByteBuffer; + +class ByteArrayNetInputStream extends NetInputStream { + + private final ByteBuffer byteBuffer; + private int index = 0; + + public ByteArrayNetInputStream(byte[] buffer) { + byteBuffer = ByteBuffer.wrap(buffer); + } + + @Override + public void markReadIndex() { + index = byteBuffer.position(); + } + + @Override + public void resetReadIndex() { + byteBuffer.position(index); + } + + @Override + public int readableBytes() { + return byteBuffer.limit(); //TODO нужно простестировать + } + + @Override + public byte readByte() { + return byteBuffer.get(); + } + + @Override + public int readBytes(byte[] buffer, int offset, int lengtn) { + return readableBytes() - byteBuffer.get(buffer, offset, lengtn).remaining(); + } + + @Override + public int readShort() { + return byteBuffer.getShort(); + } + + @Override + public int readInt() { + return byteBuffer.getInt(); + } + + @Override + public long readLong() { + return byteBuffer.getLong(); + } + + @Override + public float readFloat() { + return byteBuffer.getFloat(); + } + + @Override + public double readDouble() { + return byteBuffer.getDouble(); + } +} diff --git a/src/test/java/mc/protocol/io/NetInputStreamTest.java b/src/test/java/mc/protocol/io/NetInputStreamTest.java new file mode 100644 index 0000000..75e9cdb --- /dev/null +++ b/src/test/java/mc/protocol/io/NetInputStreamTest.java @@ -0,0 +1,300 @@ +package mc.protocol.io; + +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.*; + +class NetInputStreamTest { + + private 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); + + NetInputStream netInputStream = createNetInputStream(baos.toByteArray()); + + assertEquals(expectedValue, netInputStream.readBoolean()); + } + + @Test + void readByte() { + byte b = Integer.valueOf(random.nextInt()).byteValue(); + baos.write(b); + + assertEquals(b, createNetInputStream(baos.toByteArray()).readByte()); + assertEquals(b, createNetInputStream(baos.toByteArray()).read()); + + baos.reset(); + baos.write(128); + assertEquals(-128, createNetInputStream(baos.toByteArray()).readByte()); + } + + @Test + void readUnsignedByte() { + int value = 128; + baos.write(value); + + assertEquals(value, createNetInputStream(baos.toByteArray()).readUnsignedByte()); + } + + @Test + void readShort() throws IOException { + int value = Integer.valueOf(random.nextInt()).shortValue(); + createDataOutputStream().writeShort(value); + + assertEquals(value, createNetInputStream(baos.toByteArray()).readShort()); + + baos.reset(); + createDataOutputStream().writeShort(32768); + assertEquals(-32768, createNetInputStream(baos.toByteArray()).readShort()); + } + + @Test + void readUnsignedShort() throws IOException { + int value = 32768; + createDataOutputStream().writeShort(value); + + assertEquals(value, createNetInputStream(baos.toByteArray()).readUnsignedShort()); + } + + @ParameterizedTest + @MethodSource("paramsReadUnsignedByte") + void readUnsignedByte(byte sourceByte, int expectedValue) { + baos.write(sourceByte); + + assertEquals(expectedValue, createNetInputStream(baos.toByteArray()).readUnsignedByte()); + } + + @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); + + assertEquals(string, createNetInputStream(baos.toByteArray()).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); + + assertThrows(DecoderException.class, () -> createNetInputStream(baos.toByteArray()).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); + + assertThrows(DecoderException.class, () -> createNetInputStream(baos.toByteArray()).readString(-1)); + } + + @ParameterizedTest + @MethodSource("paramsReadVarInt") + void readVarInt(byte[] sourceBytes, int expectedValue) throws IOException { + baos.write(sourceBytes); + + assertEquals(expectedValue, createNetInputStream(baos.toByteArray()).readVarInt()); + } + + @Test + void readVarInt_tooBig() throws IOException { + baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F }); + + assertEquals(-1, createNetInputStream(baos.toByteArray()).readVarInt()); + } + + @ParameterizedTest + @MethodSource({"paramsReadVarInt", "paramsReadVarLong"}) + void readVarLong(byte[] sourceBytes, long expectedValue) throws IOException { + baos.write(sourceBytes); + + assertEquals(expectedValue, createNetInputStream(baos.toByteArray()).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 }); + + assertEquals(-1, createNetInputStream(baos.toByteArray()).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) + }); + + assertEquals(uuid, createNetInputStream(baos.toByteArray()).readUUID()); + } + + @Test + void readBytes() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + baos.write(bytes); + + byte[] actualBytes = new byte[128]; + createNetInputStream(baos.toByteArray()).readBytes(actualBytes); + + assertArrayEquals(bytes, actualBytes); + + actualBytes = new byte[128]; + int read = createNetInputStream(baos.toByteArray()).read(actualBytes); + + assertArrayEquals(bytes, actualBytes); + assertEquals(128, read); + } + + @Test + void read_offset() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + baos.write(bytes); + + byte[] actualBytes = new byte[128]; + int read = createNetInputStream(baos.toByteArray()).read(actualBytes, 3, 11); + assertEquals(11, read); + + byte[] buff1 = new byte[11]; + System.arraycopy(bytes, 0, buff1, 0, 11); + byte[] buff2 = new byte[11]; + System.arraycopy(actualBytes, 3, buff2, 0, 11); + + assertArrayEquals(buff1, buff2); + } + + private NetInputStream createNetInputStream(byte[] buffer) { + return new ByteArrayNetInputStream(buffer); + } + + private DataOutputStream createDataOutputStream() { + return new DataOutputStream(baos); + } + + private static Stream paramsReadBoolean() { + return Stream.of( + Arguments.of((byte) 0x00, false), + Arguments.of((byte) 0x01, true) + ); + } + + private static Stream paramsReadUnsignedByte() { + return Stream.of( + Arguments.of((byte) 30, 30), + Arguments.of((byte) (0xFF & 130), 130) + ); + } + + private static Stream paramsReadString() { + return Stream.of( + Arguments.of(""), + Arguments.of("Latin"), + Arguments.of("Кириллица"), + Arguments.of("العربية"), + Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления + Arguments.of("\uD800\uDD07") // Эгейские цифры, [один] + ); + } + + 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) + ); + } + + 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/NetOutputStreamTest.java b/src/test/java/mc/protocol/io/NetOutputStreamTest.java new file mode 100644 index 0000000..f63a7e8 --- /dev/null +++ b/src/test/java/mc/protocol/io/NetOutputStreamTest.java @@ -0,0 +1,230 @@ +package mc.protocol.io; + +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.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.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NetOutputStreamTest { + + private static Random random; + + @BeforeAll + static void setUp() { + random = new Random(System.currentTimeMillis()); + } + + @ParameterizedTest + @MethodSource("paramsWriteBoolean") + void writeBoolean(boolean sourceValue, byte expectedByte) { + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeBoolean(sourceValue); + + assertEquals(expectedByte, toByteArray(netOutputStream)[0]); + } + + @ParameterizedTest + @MethodSource("paramsWriteByte") + void writeByte(byte sourceValue, byte expectedByte) throws IOException { + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeByte(sourceValue); + + assertEquals(expectedByte, toByteArray(netOutputStream)[0]); + + netOutputStream = createNetOutputStream(); + netOutputStream.write(sourceValue); + + assertEquals(expectedByte, toByteArray(netOutputStream)[0]); + } + + @ParameterizedTest + @MethodSource("paramsWriteString") + void writeString(String string) { + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeString(string); + + byte[] bytes = toByteArray(netOutputStream); + int length = bytes[0]; + assertEquals(string.codePoints().count(), length); + + byte[] dataBytes = new byte[bytes.length - 1]; + System.arraycopy(bytes, 1, dataBytes, 0, dataBytes.length); + assertEquals(string, new String(dataBytes, StandardCharsets.UTF_8)); + } + + @Test + void writeString_overSize() { + String overSizeString = RandomStringUtils.randomAscii(Short.MAX_VALUE + Short.MAX_VALUE); + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeString(overSizeString); + + NetInputStream netInputStream = new ByteArrayNetInputStream(toByteArray(netOutputStream)); + String actualString = netInputStream.readString(); + + String expectedString = overSizeString.substring(0, Short.MAX_VALUE); + + assertEquals(expectedString, actualString); + } + + @ParameterizedTest + @MethodSource("paramsWriteVarInt") + void writeVarInt(int sourceValue, byte[] expectedBytes) { + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeVarInt(sourceValue); + + assertArrayEquals(expectedBytes, toByteArray(netOutputStream)); + } + + @ParameterizedTest + @MethodSource({"paramsWriteVarInt", "paramsWriteVarLong"}) + void writeVarLong(long sourceValue, byte[] expectedBytes) { + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeVarLong(sourceValue); + + assertArrayEquals(expectedBytes, toByteArray(netOutputStream)); + } + + @Test + void writeUUID() { + final UUID uuid = UUID.randomUUID(); + + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeUUID(uuid); + + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + 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) }, + toByteArray(netOutputStream)); + } + + @Test + void writeBytes() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.writeBytes(bytes); + + assertArrayEquals(bytes, toByteArray(netOutputStream)); + + netOutputStream = createNetOutputStream(); + netOutputStream.write(bytes); + + assertArrayEquals(bytes, toByteArray(netOutputStream)); + } + + @Test + void write_offset() throws IOException { + byte[] bytes = new byte[128]; + random.nextBytes(bytes); + NetOutputStream netOutputStream = createNetOutputStream(); + netOutputStream.write(bytes, 3, 11); + + byte[] actualBytes = new byte[11]; + System.arraycopy(toByteArray(netOutputStream), 0, actualBytes, 0, 11); + + byte[] expectedBytes = new byte[11]; + System.arraycopy(bytes, 3, expectedBytes, 0, 11); + + assertArrayEquals(expectedBytes, actualBytes); + } + + private NetOutputStream createNetOutputStream() { + return new ByteArrayNetOutputStream(); + } + + private byte[] toByteArray(NetOutputStream netOutputStream) { + return ((ByteArrayNetOutputStream) netOutputStream).toByteArray(); + } + + private static Stream paramsWriteBoolean() { + return Stream.of( + Arguments.of(false, (byte) 0x00), + Arguments.of(true, (byte) 0x01) + ); + } + + private static Stream paramsWriteByte() { + byte b = Integer.valueOf(random.nextInt()).byteValue(); + + return Stream.of( + Arguments.of(b, b), + Arguments.of((byte) 128, (byte) -128) + ); + } + + private static Stream paramsWriteString() { + return Stream.of( + Arguments.of(""), + Arguments.of("Latin"), + Arguments.of("Кириллица"), + Arguments.of("العربية"), + Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления + Arguments.of("\uD800\uDD07") // Эгейские цифры, [один] + ); + } + + 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 }) + ); + } + + 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 }) + ); + } +} \ No newline at end of file