diff --git a/src/main/java/im/rosetta/packet/Packet6Message.java b/src/main/java/im/rosetta/packet/Packet6Message.java index f5319d4..a573aab 100644 --- a/src/main/java/im/rosetta/packet/Packet6Message.java +++ b/src/main/java/im/rosetta/packet/Packet6Message.java @@ -4,6 +4,8 @@ import java.util.List; import im.rosetta.packet.base.PacketBaseDialog; import im.rosetta.packet.runtime.Attachment; +import im.rosetta.packet.runtime.AttachmentEncoding; +import im.rosetta.packet.runtime.AttachmentTransport; import im.rosetta.packet.runtime.AttachmentType; import io.orprotocol.Stream; @@ -58,7 +60,13 @@ public class Packet6Message extends PacketBaseDialog { String preview = stream.readString(); String blob = stream.readString(); AttachmentType type = AttachmentType.fromCode(stream.readInt8()); - this.attachments.add(new Attachment(id, blob, type, preview)); + String transportTag = stream.readString(); + String transportServer = stream.readString(); + AttachmentTransport transport = new AttachmentTransport(transportTag, transportServer); + String encodedFor = stream.readString(); + String encoder = stream.readString(); + AttachmentEncoding encoding = new AttachmentEncoding(encoder, encodedFor); + this.attachments.add(new Attachment(id, blob, type, preview, encoding, transport)); } this.aesChachaKey = stream.readString(); } @@ -80,6 +88,10 @@ public class Packet6Message extends PacketBaseDialog { stream.writeString(attachment.getPreview()); stream.writeString(attachment.getBlob()); stream.writeInt8((byte) attachment.getType().getCode()); + stream.writeString(attachment.getTransport().getTransportTag()); + stream.writeString(attachment.getTransport().getTransportServer()); + stream.writeString(attachment.getEncoding().getEncoder()); + stream.writeString(attachment.getEncoding().getEncodedFor()); } stream.writeString(this.aesChachaKey); return stream; diff --git a/src/main/java/im/rosetta/packet/runtime/Attachment.java b/src/main/java/im/rosetta/packet/runtime/Attachment.java index 7e69d06..482d6c9 100644 --- a/src/main/java/im/rosetta/packet/runtime/Attachment.java +++ b/src/main/java/im/rosetta/packet/runtime/Attachment.java @@ -9,12 +9,16 @@ public class Attachment { private String blob; private AttachmentType type; private String preview; + private AttachmentEncoding encoding; + private AttachmentTransport transport; - public Attachment(String id, String blob, AttachmentType type, String preview) { + public Attachment(String id, String blob, AttachmentType type, String preview, AttachmentEncoding encoding, AttachmentTransport transport) { this.id = id; this.blob = blob; this.type = type; this.preview = preview; + this.encoding = encoding; + this.transport = transport; } /** @@ -49,4 +53,20 @@ public class Attachment { return preview; } + /** + * Получить информацию о том, как было закодировано вложение + * @return + */ + public AttachmentEncoding getEncoding() { + return encoding; + } + + /** + * Получить информацию о том, как доставлять вложение + * @return + */ + public AttachmentTransport getTransport() { + return transport; + } + } diff --git a/src/main/java/im/rosetta/packet/runtime/AttachmentEncoding.java b/src/main/java/im/rosetta/packet/runtime/AttachmentEncoding.java new file mode 100644 index 0000000..9737754 --- /dev/null +++ b/src/main/java/im/rosetta/packet/runtime/AttachmentEncoding.java @@ -0,0 +1,24 @@ +package im.rosetta.packet.runtime; + +/** + * Класс для хранения информации о том, как было закодировано вложение в сообщении + */ +public class AttachmentEncoding { + + private String encoder; + private String encodedFor; + + public AttachmentEncoding(String encoder, String encodedFor) { + this.encoder = encoder; + this.encodedFor = encodedFor; + } + + public String getEncoder() { + return encoder; + } + + public String getEncodedFor() { + return encodedFor; + } + +} diff --git a/src/main/java/im/rosetta/packet/runtime/AttachmentTransport.java b/src/main/java/im/rosetta/packet/runtime/AttachmentTransport.java new file mode 100644 index 0000000..ba930af --- /dev/null +++ b/src/main/java/im/rosetta/packet/runtime/AttachmentTransport.java @@ -0,0 +1,21 @@ +package im.rosetta.packet.runtime; + +public class AttachmentTransport { + + private String transportTag; + private String transportServer; + + public AttachmentTransport(String transportTag, String transportServer) { + this.transportTag = transportTag; + this.transportServer = transportServer; + } + + public String getTransportTag() { + return transportTag; + } + + public String getTransportServer() { + return transportServer; + } + +} diff --git a/src/main/java/io/orprotocol/Stream.java b/src/main/java/io/orprotocol/Stream.java index 17ed755..1cf6ba4 100644 --- a/src/main/java/io/orprotocol/Stream.java +++ b/src/main/java/io/orprotocol/Stream.java @@ -2,16 +2,14 @@ package io.orprotocol; import java.util.Arrays; - /** - * Это класс для чтения и записи данных в виде потока байтов. - * Он предоставляет методы для записи и чтения различных типов данных, таких как целые числа, - * строки и массивы байтов. Поток может быть расширен динамически при записи данных, и он обеспечивает - * эффективное управление битами для оптимального использования пространства. Этот класс используется в - * протоколе для сериализации и десериализации пакетов данных, которые передаются между клиентами и сервером. - * - * writeInt<> - это методы для записи signed чисел (с учетом знака), которые используют дополнительный бит для хранения информации о знаке числа. - * writeUInt<> - это методы для записи unsigned чисел (без учета знака), которые используют все биты для хранения значения числа. + * Поток побитовой/побайтовой записи и чтения. + * + * Поддержка: + * - signed: Int8/16/32/64 (классический two's complement) + * - unsigned: UInt8/16/32/64 + * - String: длина UInt32 + символы UInt16 + * - byte[]: длина UInt32 + сырые байты */ public class Stream { @@ -39,98 +37,27 @@ public class Stream { this.writePointer = this.stream.length << 3; } - public void writeByte(byte b){ - writeUInt8(b); + public byte[] getBuffer() { + return getStream(); } - public byte readByte() { - return (byte) readUInt8(); + public boolean isEmpty() { + return writePointer == 0; } - public void writeUInt8(int value) { - reserveBits(8); - int byteIndex = writePointer >> 3; - stream[byteIndex] = (byte) (value & 0xFF); - writePointer += 8; + /** Количество реально записанных байт (без резервной емкости). */ + public int length() { + return (writePointer + 7) >> 3; } - public int readUInt8() { - if (remainingBits() < 8L) { - throw new IllegalStateException("Not enough bits to read UInt8"); - } - int byteIndex = readPointer >> 3; - int value = stream[byteIndex] & 0xFF; - readPointer += 8; - return value; - } - - public void writeUInt16(int value) { - reserveBits(16); - writeUInt8(value >> 8); - writeUInt8(value & 0xFF); - } - - public int readUInt16() { - int high = readUInt8(); - int low = readUInt8(); - return (high << 8) | low; - } - - public void writeUInt32(int value) { - reserveBits(32); - writeUInt16((int) (value >> 16)); - writeUInt16((int) (value & 0xFFFF)); - } - - public int readUInt32() { - int high = readUInt16(); - int low = readUInt16(); - return (high << 16) | low; - } - - public void writeUInt64(long value) { - reserveBits(64); - writeUInt32((int) (value >> 32)); - writeUInt32((int) (value & 0xFFFFFFFFL)); - } - - public long readUInt64() { - long high = readUInt32(); - long low = readUInt32(); - return (high << 32) | low; - } - - /** - * Записывает signed 8-битное целое в классическом two's complement (ровно 8 бит). - */ - public void writeInt8(int value) { - writeUInt8(value); // берутся младшие 8 бит - } - - /** - * Читает signed 8-битное целое в классическом two's complement. - */ - public int readInt8() { - /** - * Приводим к byte чтобы сохранить знак - */ - return (byte) readUInt8(); - } + // ---------- bit / boolean ---------- public void writeBit(int value) { - reserveBits(1); - int bit = value & 1; - stream[writePointer >> 3] |= (byte) (bit << (7 - (writePointer & 7))); - writePointer++; + writeBits(value & 1L, 1); } public int readBit() { - if (remainingBits() < 1L) { - throw new IllegalStateException("Not enough bits to read bit"); - } - int bit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; - readPointer++; - return bit; + return (int) readBits(1); } public void writeBoolean(boolean value) { @@ -141,20 +68,129 @@ public class Stream { return readBit() == 1; } + // ---------- byte ---------- + + public void writeByte(byte b) { + writeUInt8(b & 0xFF); + } + + public byte readByte() { + return (byte) readUInt8(); + } + + // ---------- UInt / Int 8 ---------- + + public void writeUInt8(int value) { + int v = value & 0xFF; + + // fast path when byte-aligned + if ((writePointer & 7) == 0) { + reserveBits(8); + stream[writePointer >> 3] = (byte) v; + writePointer += 8; + return; + } + + writeBits(v, 8); + } + + public int readUInt8() { + if (remainingBits() < 8L) { + throw new IllegalStateException("Not enough bits to read UInt8"); + } + + // fast path when byte-aligned + if ((readPointer & 7) == 0) { + int value = stream[readPointer >> 3] & 0xFF; + readPointer += 8; + return value; + } + + return (int) readBits(8); + } + + public void writeInt8(int value) { + writeUInt8(value); // lower 8 bits (two's complement) + } + + public int readInt8() { + return (byte) readUInt8(); // sign extension + } + + // ---------- UInt / Int 16 ---------- + + public void writeUInt16(int value) { + int v = value & 0xFFFF; + writeUInt8((v >>> 8) & 0xFF); + writeUInt8(v & 0xFF); + } + + public int readUInt16() { + int hi = readUInt8(); + int lo = readUInt8(); + return (hi << 8) | lo; + } + public void writeInt16(int value) { - writeUInt16(value); + writeUInt16(value); // lower 16 bits (two's complement) } public int readInt16() { - return (short) readUInt16(); + return (short) readUInt16(); // sign extension + } + + // ---------- UInt / Int 32 ---------- + + public void writeUInt32(long value) { + if (value < 0 || value > 0xFFFFFFFFL) { + throw new IllegalArgumentException("UInt32 out of range: " + value); + } + + writeUInt8((int) ((value >>> 24) & 0xFF)); + writeUInt8((int) ((value >>> 16) & 0xFF)); + writeUInt8((int) ((value >>> 8) & 0xFF)); + writeUInt8((int) (value & 0xFF)); + } + + public long readUInt32() { + long b1 = readUInt8() & 0xFFL; + long b2 = readUInt8() & 0xFFL; + long b3 = readUInt8() & 0xFFL; + long b4 = readUInt8() & 0xFFL; + return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; } public void writeInt32(int value) { - writeUInt32(value); + writeUInt32(value & 0xFFFFFFFFL); } public int readInt32() { - return readUInt32(); + return (int) readUInt32(); + } + + // ---------- UInt / Int 64 ---------- + + /** + * Записывает 64 бита как есть (для UInt64 это битовый паттерн в long). + */ + public void writeUInt64(long value) { + writeUInt8((int) ((value >>> 56) & 0xFF)); + writeUInt8((int) ((value >>> 48) & 0xFF)); + writeUInt8((int) ((value >>> 40) & 0xFF)); + writeUInt8((int) ((value >>> 32) & 0xFF)); + writeUInt8((int) ((value >>> 24) & 0xFF)); + writeUInt8((int) ((value >>> 16) & 0xFF)); + writeUInt8((int) ((value >>> 8) & 0xFF)); + writeUInt8((int) (value & 0xFF)); + } + + /** + * Читает 64 бита как есть (битовый паттерн в long). + */ + public long readUInt64() { + long high = readUInt32() & 0xFFFFFFFFL; + long low = readUInt32() & 0xFFFFFFFFL; + return (high << 32) | low; } public void writeInt64(long value) { @@ -165,28 +201,26 @@ public class Stream { return readUInt64(); } + // ---------- float ---------- + public void writeFloat32(float value) { - int floatValue = Float.floatToIntBits(value); - writeInt32(floatValue); + writeInt32(Float.floatToIntBits(value)); } public float readFloat32() { - int floatValue = readInt32(); - return Float.intBitsToFloat(floatValue); + return Float.intBitsToFloat(readInt32()); } + // ---------- string / bytes ---------- + /** - * Записывает строку в поток. Сначала записывается длина строки (как unsigned 32-битное целое), а затем записываются символы строки, которые кодируются как 16-битные значения. - * @param value строка для записи в поток. Если строка null, она будет записана как пустая строка. Длина строки ограничена максимальным значением unsigned 32-битного целого. + * String: length(UInt32) + chars(UInt16). */ public void writeString(String value) { if (value == null) { value = ""; } - /** - * Пишем длину строки как unsigned 32-битное целое, чтобы при чтении знать, сколько символов нужно прочитать для восстановления строки - */ int length = value.length(); writeUInt32(length); @@ -194,137 +228,137 @@ public class Stream { return; } - /** - * Резервируем по 16 бит на каждый символ строки, так как каждый символ кодируется как 16-битное значение - */ reserveBits((long) length * 16L); - for (int i = 0; i < length; i++) { writeUInt16(value.charAt(i)); } } - /** - * Читает строку из потока. Сначала читается длина строки (как unsigned 32-битное целое), а затем читаются символы строки, которые кодируются как 16-битные значения. - * Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества символов, возвращается пустая строка. - * @return строка, прочитанная из потока - */ public String readString() { - /** - * Читаем длину как UInt32 - */ - int length = readUInt32(); - - if (length < 0) { - return ""; + long lenLong = readUInt32(); + if (lenLong > Integer.MAX_VALUE) { + throw new IllegalStateException("String length too large: " + lenLong); } + int length = (int) lenLong; long requiredBits = (long) length * 16L; if (requiredBits > remainingBits()) { throw new IllegalStateException("Not enough bits to read string"); } - StringBuilder value = new StringBuilder(length); + StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { - /** - * Читаем символы строки как UInt16 и добавляем их в StringBuilder. Каждый символ кодируется 16 битами, - * что позволяет поддерживать широкий диапазон символов Unicode. - */ - value.append((char) readUInt16()); + sb.append((char) readUInt16()); } - return value.toString(); + return sb.toString(); } /** - * Записывает массив байтов в поток. - * Сначала записывается длина массива (как unsigned 32-битное целое), - * а затем записываются сами байты. Если массив null, он будет записан как массив нулевой длины. - * @param value массив байтов для записи в поток + * byte[]: length(UInt32) + raw bytes. */ public void writeBytes(byte[] value) { if (value == null) { value = new byte[0]; } - /** - * Пишем длинну массива как unsigned 32-битное целое, чтобы при чтении знать, сколько байтов нужно прочитать для восстановления массива. - */ + writeUInt32(value.length); - if (value.length > 0) { - /** - * Резервируем нужное количество бит для записи массива байт. На один байт нужно 8 бит, поэтому умножаем длину - * массива на 8, чтобы получить общее количество бит для резервирования. - */ - reserveBits((long) value.length * 8L); + if (value.length == 0) { + return; + } + + reserveBits((long) value.length * 8L); + + // fast path when byte-aligned + if ((writePointer & 7) == 0) { + int byteIndex = writePointer >> 3; + ensureCapacity(byteIndex + value.length - 1); + System.arraycopy(value, 0, stream, byteIndex, value.length); + writePointer += (value.length << 3); + return; } for (byte b : value) { - writeByte(b); + writeUInt8(b & 0xFF); } } - /** - * Читает массив байтов из потока. Сначала читается длина массива (как unsigned 32-битное целое), - * а затем читаются сами байты. Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества байтов, возвращается пустой массив. - * @return массив байтов, прочитанный из потока - */ public byte[] readBytes() { - int length = readUInt32(); - - if (length <= 0) { + long lenLong = readUInt32(); + if (lenLong == 0) { return new byte[0]; } + if (lenLong > Integer.MAX_VALUE) { + throw new IllegalStateException("Byte array too large: " + lenLong); + } + int length = (int) lenLong; long requiredBits = (long) length * 8L; if (requiredBits > remainingBits()) { return new byte[0]; } - byte[] value = new byte[length]; - for (int i = 0; i < length; i++) { - value[i] = readByte(); + byte[] out = new byte[length]; + + // fast path when byte-aligned + if ((readPointer & 7) == 0) { + int byteIndex = readPointer >> 3; + System.arraycopy(stream, byteIndex, out, 0, length); + readPointer += (length << 3); + return out; } - return value; + + for (int i = 0; i < length; i++) { + out[i] = (byte) readUInt8(); + } + return out; } - public boolean isEmpty() { - return writePointer == 0; - } - - /** - * Возвращает количество реально использованного места (то есть только используемые байты, а не весь буфер включая аллокации) - * @return количество реально использованных байтов - */ - public int length() { - /** - * Так как writePointer - это индекс в битах, то для получения количества байтов нужно разделить его на 8. - * Но так как writePointer может быть не кратен 8, то нужно округлить его в большую сторону, чтобы учесть все использованные биты. - */ - return (writePointer + 7) >> 3; - } + // ---------- internals ---------- - public byte[] getBuffer() { - return getStream(); - } - - /** - * Возвращает количество оставшихся битов для чтения. Это разница между указателем записи и указателем чтения, - * которая показывает, сколько битов еще доступно для чтения в потоке. - * @return количество оставшихся битов для чтения - */ private long remainingBits() { return (long) writePointer - readPointer; } - /** - * Резервирует место в потоке для записи указанного количества битов. Этот метод проверяет, достаточно ли места в текущем буфере для записи новых данных, - * и если нет, то расширяет буфер до необходимого размера. - * Это позволяет избежать переполнения, а вызывающему коду не беспокоится о дополнительных аллокациях при записи данных в поток. - * @param bitsToWrite количество битов, которые нужно зарезервировать для записи - * @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов, - * который хранит данные потока. Он гарантирует, что при записи данных в поток всегда будет достаточно места для хранения - * этих данных, и при необходимости расширяет буфер. - */ + private void writeBits(long value, int bits) { + if (bits <= 0) { + return; + } + + reserveBits(bits); + + for (int i = bits - 1; i >= 0; i--) { + int bit = (int) ((value >>> i) & 1L); + int byteIndex = writePointer >> 3; + int shift = 7 - (writePointer & 7); + + if (bit == 1) { + stream[byteIndex] = (byte) (stream[byteIndex] | (1 << shift)); + } else { + stream[byteIndex] = (byte) (stream[byteIndex] & ~(1 << shift)); + } + + writePointer++; + } + } + + private long readBits(int bits) { + if (bits <= 0) { + return 0; + } + if (remainingBits() < bits) { + throw new IllegalStateException("Not enough bits to read"); + } + + long value = 0; + for (int i = 0; i < bits; i++) { + int bit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; + value = (value << 1) | bit; + readPointer++; + } + return value; + } + private void reserveBits(long bitsToWrite) { if (bitsToWrite <= 0) { return;