From 28b084cd9c0dac43049bd6725a98be30db360d26 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 31 May 2026 21:32:22 +0300 Subject: [PATCH 1/5] =?UTF-8?q?-=20**BinaryReader**:=20=20=20-=20**New=20F?= =?UTF-8?q?eature**:=20Added=20random=20access=20read=20methods=20?= =?UTF-8?q?=E2=80=94=20`getUint8(int=20position)`,=20`getInt16(int=20posit?= =?UTF-8?q?ion)`,=20`getUint16(int=20position)`,=20`getInt32(int=20positio?= =?UTF-8?q?n)`,=20`getUint32(int=20position)`,=20`getInt64(int=20position)?= =?UTF-8?q?`,=20`getUint64(int=20position)`,=20`getFloat32(int=20position)?= =?UTF-8?q?`,=20`getFloat64(int=20position)`=20=E2=80=94=20read=20at=20arb?= =?UTF-8?q?itrary=20position=20without=20advancing=20offset.=20=20=20-=20*?= =?UTF-8?q?*Refactoring**:=20Rewritten=20using=20`extension=20type`=20for?= =?UTF-8?q?=20zero-cost=20abstractions.=20=20=20-=20**Refactoring**:=20Met?= =?UTF-8?q?hods=20organized=20into=20semantic=20extension=20groups=20(`Bin?= =?UTF-8?q?aryReaderCore`,=20`BinaryReaderVarInt`,=20`BinaryReaderNumeric`?= =?UTF-8?q?,=20`BinaryReaderBytesString`,=20`BinaryReaderRandomAccess`,=20?= =?UTF-8?q?`BinaryReaderPosition`,=20`BinaryReaderInternal`).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **BinaryWriter**: - **New Feature**: Added random access read methods — `getUint8(int position)`, `getInt16(int position)`, `getUint16(int position)`, `getInt32(int position)`, `getUint32(int position)`, `getInt64(int position)`, `getUint64(int position)`, `getFloat32(int position)`, `getFloat64(int position)` — read written bytes at arbitrary position. - **New Feature**: Added random access write methods — `setUint8(int position, int value)`, `setInt16(int position, int value)`, `setUint16(int position, int value)`, `setInt32(int position, int value)`, `setUint32(int position, int value)`, `setInt64(int position, int value)`, `setUint64(int position, int value)`, `setFloat32(int position, double value)`, `setFloat64(int position, double value)` — write at arbitrary position without changing offset. - **Refactoring**: Rewritten using `extension type` for zero-cost abstractions. - **Refactoring**: Methods organized into semantic extension groups (`BinaryWriterCore`, `BinaryWriterVarInt`, `BinaryWriterNumeric`, `BinaryWriterBytesString`, `BinaryWriterRandomAccess`, `BinaryWriterPosition`, `BinaryWriterInternal`). - **Tests**: - Added `test/unit/binary_reader_get_test.dart` — 41 tests for random access reads. - Added `test/unit/binary_writer_set_get_test.dart` — 49 tests for random access reads/writes. --- CHANGELOG.md | 17 + README.md | 15 +- lib/src/binary_reader.dart | 292 +++++++++--- lib/src/binary_writer.dart | 554 +++++++++++++++++----- pubspec.yaml | 2 +- test/unit/binary_reader_get_test.dart | 410 ++++++++++++++++ test/unit/binary_writer_set_get_test.dart | 449 ++++++++++++++++++ 7 files changed, 1553 insertions(+), 186 deletions(-) create mode 100644 test/unit/binary_reader_get_test.dart create mode 100644 test/unit/binary_writer_set_get_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a8d79..66187c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ +# 5.3.0 + +- **BinaryReader**: + - **New Feature**: Added random access read methods — `getUint8(int position)`, `getInt16(int position)`, `getUint16(int position)`, `getInt32(int position)`, `getUint32(int position)`, `getInt64(int position)`, `getUint64(int position)`, `getFloat32(int position)`, `getFloat64(int position)` — read at arbitrary position without advancing offset. + - **Refactoring**: Rewritten using `extension type` for zero-cost abstractions. + - **Refactoring**: Methods organized into semantic extension groups (`BinaryReaderCore`, `BinaryReaderVarInt`, `BinaryReaderNumeric`, `BinaryReaderBytesString`, `BinaryReaderRandomAccess`, `BinaryReaderPosition`, `BinaryReaderInternal`). + +- **BinaryWriter**: + - **New Feature**: Added random access read methods — `getUint8(int position)`, `getInt16(int position)`, `getUint16(int position)`, `getInt32(int position)`, `getUint32(int position)`, `getInt64(int position)`, `getUint64(int position)`, `getFloat32(int position)`, `getFloat64(int position)` — read written bytes at arbitrary position. + - **New Feature**: Added random access write methods — `setUint8(int position, int value)`, `setInt16(int position, int value)`, `setUint16(int position, int value)`, `setInt32(int position, int value)`, `setUint32(int position, int value)`, `setInt64(int position, int value)`, `setUint64(int position, int value)`, `setFloat32(int position, double value)`, `setFloat64(int position, double value)` — write at arbitrary position without changing offset. + - **Refactoring**: Rewritten using `extension type` for zero-cost abstractions. + - **Refactoring**: Methods organized into semantic extension groups (`BinaryWriterCore`, `BinaryWriterVarInt`, `BinaryWriterNumeric`, `BinaryWriterBytesString`, `BinaryWriterRandomAccess`, `BinaryWriterPosition`, `BinaryWriterInternal`). + +- **Tests**: + - Added `test/unit/binary_reader_get_test.dart` — 41 tests for random access reads. + - Added `test/unit/binary_writer_set_get_test.dart` — 49 tests for random access reads/writes. + # 5.2.0 - **BinaryWriter**: diff --git a/README.md b/README.md index d175344..8575ad8 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,22 @@ ```yaml dependencies: - pro_binary: ^5.2.0 + pro_binary: last_version ``` +or: + +Add `pro_binary` as a dependency to your project: + +```bash +dart pub add pro_binary +``` + +Or for Flutter projects: + +```bash +flutter pub add pro_binary + ## Quick Start ```dart diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 8ea8c1e..25fbf32 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -48,7 +48,10 @@ extension type BinaryReader._(_ReaderState _rs) { /// ``` factory BinaryReader.fromList(List buffer) => BinaryReader(Uint8List.fromList(buffer)); +} +/// Core properties and operators for [BinaryReader]. +extension BinaryReaderCore on BinaryReader { /// Returns the number of bytes remaining to be read. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @@ -64,6 +67,46 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int get length => _rs.length; + /// Returns the byte at the specified absolute [index] in the buffer. + /// + /// This allows random access without affecting the current [offset]. + /// + /// Example: + /// ```dart + /// final firstByte = reader[0]; + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int operator [](int index) => _rs.list[index]; + + /// Reads [length] bytes from the current position. + /// + /// This is a concise alias for [readBytes]. + /// + /// Example: + /// ```dart + /// final data = reader(10); // Same as reader.readBytes(10) + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List call(int length) => readBytes(length); + + /// Rebinds the reader to a new buffer without creating a new [BinaryReader]. + /// + /// Resets the read position and replaces the internal buffer with [buffer]. + /// This is useful for streaming scenarios where you want to reuse a reader + /// with new data without allocating a new [BinaryReader] or [_ReaderState]. + /// + /// After rebinding, the reader starts at position 0 of the new buffer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void rebind(Uint8List buffer) { + _rs.rebind(buffer); + } +} + +/// Variable-length integer reading methods for [BinaryReader]. +extension BinaryReaderVarInt on BinaryReader { /// Reads an unsigned variable-length integer encoded using VarInt format. /// /// VarInt encoding uses the lower 7 bits of each byte for data and the @@ -186,7 +229,10 @@ extension type BinaryReader._(_ReaderState _rs) { // Decode: right shift by 1, XOR with sign-extended LSB return (v >>> 1) ^ -(v & 1); } +} +/// Fixed-width numeric reading methods for [BinaryReader]. +extension BinaryReaderNumeric on BinaryReader { /// Reads an 8-bit unsigned integer (0-255). /// /// Example: @@ -393,7 +439,10 @@ extension type BinaryReader._(_ReaderState _rs) { return value; } +} +/// Byte array and string reading methods for [BinaryReader]. +extension BinaryReaderBytesString on BinaryReader { /// Reads a sequence of bytes and returns them as a [Uint8List]. /// /// Returns a view of the underlying buffer without copying data, @@ -565,15 +614,6 @@ extension type BinaryReader._(_ReaderState _rs) { return readString(length, allowMalformed: allowMalformed); } - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - int _readLength(LengthEncoding encoding) => switch (encoding) { - .u8 => readUint8(), - .u16 => readUint16(), - .u32 => readUint32(), - .u64 => readUint64(), - }; - /// Reads a boolean value (1 byte). /// /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. @@ -590,29 +630,168 @@ extension type BinaryReader._(_ReaderState _rs) { return value != 0; } +} - /// Checks if there are at least [length] bytes available to read. +/// Random access and position inspection methods for [BinaryReader]. +extension BinaryReaderRandomAccess on BinaryReader { + /// Reads a byte at the specified [position] without changing the current + /// read position. /// - /// Returns `true` if enough bytes are available, `false` otherwise. + /// Throws [RangeError] if [position] is negative or beyond the buffer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint8(int position) { + assert(position >= 0 && position < _rs.length, 'position out of bounds'); + return _rs.list[position]; + } + + /// Reads a 16-bit signed integer at the specified [position] without changing + /// the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 1]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt16(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 2 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getInt16(position, endian); + } + + /// Reads a 16-bit unsigned integer at the specified [position] without + /// changing the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 1]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint16(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 2 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getUint16(position, endian); + } + + /// Reads a 32-bit signed integer at the specified [position] without changing + /// the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getInt32(position, endian); + } + + /// Reads a 32-bit unsigned integer at the specified [position] without + /// changing the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getUint32(position, endian); + } + + /// Reads a 64-bit signed integer at the specified [position] without changing + /// the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getInt64(position, endian); + } + + /// Reads a 64-bit unsigned integer at the specified [position] without + /// changing the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getUint64(position, endian); + } + + /// Reads a 32-bit floating-point number at the specified [position] without + /// changing the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double getFloat32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getFloat32(position, endian); + } + + /// Reads a 64-bit floating-point number at the specified [position] without + /// changing the current read position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double getFloat64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _rs.length, + 'position out of bounds', + ); + return _rs.data.getFloat64(position, endian); + } + + /// Returns the byte at the current read position without advancing the + /// offset. + /// + /// This is a convenience method for peeking at the next byte to be read. /// - /// Useful for conditional reads when the data format may vary. /// Example: /// ```dart - /// if (reader.hasBytes(4)) { - /// final value = reader.readUint32(); - /// // Process value - /// } else { - /// // Handle missing data + /// final nextByte = reader.peekByte(); + /// if (nextByte == 0x42) { + /// // Handle type 0x42 /// } + /// final actualByte = reader.readUint8(); // Now read it /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - bool hasBytes(int length) { - if (length < 0) { - throw RangeError.value(length, 'length', 'Length must be non-negative'); - } + int peekByte() { + _checkBounds(1, 'Peek Byte'); - return (_rs.offset + length) <= _rs.length; + return _rs.list[_rs.offset]; } /// Reads bytes without advancing the read position. @@ -655,26 +834,32 @@ extension type BinaryReader._(_ReaderState _rs) { return view; } +} - /// Returns the byte at the current read position without advancing the - /// offset. +/// Position management methods for [BinaryReader]. +extension BinaryReaderPosition on BinaryReader { + /// Checks if there are at least [length] bytes available to read. /// - /// This is a convenience method for peeking at the next byte to be read. + /// Returns `true` if enough bytes are available, `false` otherwise. /// + /// Useful for conditional reads when the data format may vary. /// Example: /// ```dart - /// final nextByte = reader.peekByte(); - /// if (nextByte == 0x42) { - /// // Handle type 0x42 + /// if (reader.hasBytes(4)) { + /// final value = reader.readUint32(); + /// // Process value + /// } else { + /// // Handle missing data /// } - /// final actualByte = reader.readUint8(); // Now read it /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - int peekByte() { - _checkBounds(1, 'Peek Byte'); + bool hasBytes(int length) { + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } - return _rs.list[_rs.offset]; + return (_rs.offset + length) <= _rs.length; } /// Advances the read position by the specified number of bytes. @@ -751,43 +936,18 @@ extension type BinaryReader._(_ReaderState _rs) { _rs.offset -= length; } +} - /// Rebinds the reader to a new buffer without creating a new [BinaryReader]. - /// - /// Resets the read position and replaces the internal buffer with [buffer]. - /// This is useful for streaming scenarios where you want to reuse a reader - /// with new data without allocating a new [BinaryReader] or [_ReaderState]. - /// - /// After rebinding, the reader starts at position 0 of the new buffer. - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void rebind(Uint8List buffer) { - _rs.rebind(buffer); - } - - /// Returns the byte at the specified absolute [index] in the buffer. - /// - /// This allows random access without affecting the current [offset]. - /// - /// Example: - /// ```dart - /// final firstByte = reader[0]; - /// ``` - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - int operator [](int index) => _rs.list[index]; - - /// Reads [length] bytes from the current position. - /// - /// This is a concise alias for [readBytes]. - /// - /// Example: - /// ```dart - /// final data = reader(10); // Same as reader.readBytes(10) - /// ``` +/// Internal methods for [BinaryReader]. +extension _BinaryReaderInternal on BinaryReader { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - Uint8List call(int length) => readBytes(length); + int _readLength(LengthEncoding encoding) => switch (encoding) { + .u8 => readUint8(), + .u16 => readUint16(), + .u32 => readUint32(), + .u64 => readUint64(), + }; /// Internal method to check if enough bytes are available to read. /// diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 4efce0a..b656349 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -39,13 +39,129 @@ extension type BinaryWriter._(_WriterState _ws) { /// [initialBufferSize] defaults to 128 bytes. BinaryWriter({int initialBufferSize = 128}) : this._(_WriterState(initialBufferSize)); +} +/// Core properties and operators for [BinaryWriter]. +extension BinaryWriterCore on BinaryWriter { /// Returns the total number of bytes written to the buffer. int get bytesWritten => _ws.offset; /// Returns the current capacity of the internal buffer. int get capacity => _ws.capacity; + /// Returns the byte at the specified [index] without changing the current + /// write position. + /// + /// Throws [RangeError] if [index] is negative or greater than or equal to + /// [bytesWritten]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int operator [](int index) { + if (index < 0 || index >= _ws.offset) { + throw RangeError.range(index, 0, _ws.offset - 1, 'index'); + } + return _ws.list[index]; + } + + /// Writes a byte at the specified [index] without changing the current + /// write position. + /// + /// This operator is used to overwrite already written bytes. To append data, + /// use the standard `write*` methods. + /// + /// Throws [RangeError] if [index] is negative or greater than or equal to + /// [bytesWritten]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void operator []=(int index, int value) { + if (index < 0 || index >= _ws.offset) { + throw RangeError.range(index, 0, _ws.offset - 1, 'index'); + } + + _checkRange(value, 0, 255, 'Uint8'); + _ws.list[index] = value; + } + + /// Writes a sequence of bytes. + /// + /// This is a concise alias for [writeBytes]. + /// + /// Example: + /// ```dart + /// writer([1, 2, 3]); // Same as writer.writeBytes([1, 2, 3]) + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void call(List bytes) => writeBytes(bytes); + + /// Extracts all written bytes and resets the writer. + /// + /// [copy] determines how the bytes are extracted: + /// - If `true`: The written bytes are copied into a new [Uint8List]. The + /// internal buffer is retained and its offset is reset to 0. This is + /// highly efficient for pooling (e.g., [BinaryWriterPool]) as the same + /// large buffer is reused for subsequent operations without re-allocation. + /// - If `false` (default): A view of the internal buffer is returned, and + /// the writer detaches from it by allocating a fresh initial-sized buffer. + /// While the returned bytes are "zero-copy" relative to the old buffer, + /// this forces the writer to re-allocate memory, which is less efficient + /// for pooling long-term. + /// + /// After calling this method, the writer is reset and ready for reuse. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// // For best pooling performance (retains internal buffer): + /// final packet = writer.takeBytes(copy: true); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List takeBytes({bool copy = false}) { + if (copy) { + final result = _ws.list.sublist(0, _ws.offset); + _ws.offset = 0; + + return result; + } + + final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); + _ws._initializeBuffer(); + + return result; + } + + /// Returns a view of the written bytes (from index 0 up to the current + /// [bytesWritten]) without resetting the writer. + /// + /// Unlike [takeBytes], this does not reset the writer's state. + /// Subsequent writes will continue appending to the buffer. + /// + /// **Note:** Since this returns a view, the content of the returned list + /// will change if you continue writing to this writer. + /// + /// **Use case:** When you need to inspect or copy data mid-stream. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// final snapshot = writer.toBytes(); // Peek at current data + /// writer.writeUint32(100); // Continue writing + /// final final = writer.takeBytes(); // Get all data + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); + + /// Resets the writer to its initial state, discarding all written data. + @pragma('vm:prefer-inline') + void reset() => _ws._initializeBuffer(); +} + +/// Variable-length integer writing methods for [BinaryWriter]. +extension BinaryWriterVarInt on BinaryWriter { /// Writes an unsigned variable-length integer using VarInt encoding. /// /// VarInt encoding uses the lower 7 bits of each byte for data and the @@ -162,7 +278,10 @@ extension type BinaryWriter._(_WriterState _ws) { final encoded = (value << 1) ^ (value >> 63); writeVarUint(encoded); } +} +/// Fixed-width numeric writing methods for [BinaryWriter]. +extension BinaryWriterNumeric on BinaryWriter { /// Writes a boolean value as a single byte. /// /// `true` is written as `1` and `false` as `0`. @@ -375,7 +494,10 @@ extension type BinaryWriter._(_WriterState _ws) { _ws.data.setFloat64(_ws.offset, value, endian); _ws.offset += 8; } +} +/// Byte array and string writing methods for [BinaryWriter]. +extension BinaryWriterBytesString on BinaryWriter { /// Writes a sequence of bytes from the given list. /// /// [offset] specifies the starting position in [bytes] (defaults to 0). @@ -673,16 +795,6 @@ extension type BinaryWriter._(_WriterState _ws) { _ws.offset = currentOffset; } - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - int _varIntSize(int value) => switch (value) { - < 0x80 => 1, - < 0x4000 => 2, - < 0x200000 => 3, - < 0x10000000 => 4, - _ => 5, - }; - /// Writes a UTF-8 encoded string prefixed with a fixed-width length. /// /// The length prefix size is determined by [lengthEncoding]. @@ -729,95 +841,332 @@ extension type BinaryWriter._(_WriterState _ws) { _writeLength(byteLength, lengthEncoding); _ws.offset = finalOffset; } +} +/// Random access read/write methods for [BinaryWriter]. +extension BinaryWriterRandomAccess on BinaryWriter { + /// Reads a byte at the specified [position] without changing the current + /// write position. + /// + /// Throws [RangeError] if [position] is negative or greater than or equal to + /// [bytesWritten]. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _writeLength(int length, LengthEncoding encoding) { - switch (encoding) { - case .u8: - writeUint8(length); - case .u16: - writeUint16(length); - case .u32: - writeUint32(length); - case .u64: - writeUint64(length); + int getUint8(int position) { + assert(position >= 0 && position < _ws.offset, 'position out of bounds'); + return _ws.list[position]; + } + + /// Reads a 16-bit signed integer at the specified [position] without changing + /// the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 1]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt16(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 2 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getInt16(position, endian); + } + + /// Reads a 16-bit unsigned integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 1]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint16(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 2 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getUint16(position, endian); + } + + /// Reads a 32-bit signed integer at the specified [position] without changing + /// the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getInt32(position, endian); + } + + /// Reads a 32-bit unsigned integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getUint32(position, endian); + } + + /// Reads a 64-bit signed integer at the specified [position] without changing + /// the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getInt64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getInt64(position, endian); + } + + /// Reads a 64-bit unsigned integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int getUint64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getUint64(position, endian); + } + + /// Reads a 32-bit floating-point number at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double getFloat32(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 4 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getFloat32(position, endian); + } + + /// Reads a 64-bit floating-point number at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double getFloat64(int position, [Endian endian = Endian.big]) { + assert( + position >= 0 && position + 8 <= _ws.offset, + 'position out of bounds', + ); + return _ws.data.getFloat64(position, endian); + } + + /// Writes a byte at the specified [position] without changing the current + /// write position. + /// + /// Throws [RangeError] if [position] is negative or greater than or equal to + /// [bytesWritten]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setUint8(int position, int value) { + if (position < 0 || position >= _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 1, 'position'); } + _checkRange(value, 0, 255, 'Uint8'); + _ws.list[position] = value; } - /// Writes a sequence of bytes. + /// Writes a 16-bit signed integer at the specified [position] without + /// changing the current write position. /// - /// This is a concise alias for [writeBytes]. + /// [endian] specifies byte order (defaults to big-endian). /// - /// Example: - /// ```dart - /// writer([1, 2, 3]); // Same as writer.writeBytes([1, 2, 3]) - /// ``` + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 1]. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void call(List bytes) => writeBytes(bytes); + void setInt16(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 2 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 2, 'position'); + } + _checkRange(value, -32768, 32767, 'Int16'); + _ws.data.setInt16(position, value, endian); + } - /// Extracts all written bytes and resets the writer. + /// Writes a 16-bit unsigned integer at the specified [position] without + /// changing the current write position. /// - /// [copy] determines how the bytes are extracted: - /// - If `true`: The written bytes are copied into a new [Uint8List]. The - /// internal buffer is retained and its offset is reset to 0. This is - /// highly efficient for pooling (e.g., [BinaryWriterPool]) as the same - /// large buffer is reused for subsequent operations without re-allocation. - /// - If `false` (default): A view of the internal buffer is returned, and - /// the writer detaches from it by allocating a fresh initial-sized buffer. - /// While the returned bytes are "zero-copy" relative to the old buffer, - /// this forces the writer to re-allocate memory, which is less efficient - /// for pooling long-term. + /// [endian] specifies byte order (defaults to big-endian). /// - /// After calling this method, the writer is reset and ready for reuse. + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 1]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setUint16(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 2 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 2, 'position'); + } + _checkRange(value, 0, 65535, 'Uint16'); + _ws.data.setUint16(position, value, endian); + } + + /// Writes a 32-bit signed integer at the specified [position] without + /// changing the current write position. /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint32(42); - /// // For best pooling performance (retains internal buffer): - /// final packet = writer.takeBytes(copy: true); - /// ``` + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - Uint8List takeBytes({bool copy = false}) { - if (copy) { - final result = _ws.list.sublist(0, _ws.offset); - _ws.offset = 0; + void setInt32(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 4 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 4, 'position'); + } + _checkRange(value, -2147483648, 2147483647, 'Int32'); + _ws.data.setInt32(position, value, endian); + } - return result; + /// Writes a 32-bit unsigned integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setUint32(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 4 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 4, 'position'); } + _checkRange(value, 0, 4294967295, 'Uint32'); + _ws.data.setUint32(position, value, endian); + } - final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); - _ws._initializeBuffer(); + /// Writes a 64-bit signed integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setInt64(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 8 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 8, 'position'); + } + _checkRange(value, kMinInt64, kMaxInt64, 'Int64'); + _ws.data.setInt64(position, value, endian); + } - return result; + /// Writes a 64-bit unsigned integer at the specified [position] without + /// changing the current write position. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setUint64(int position, int value, [Endian endian = Endian.big]) { + if (position < 0 || position + 8 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 8, 'position'); + } + _checkRange(value, 0, kMaxInt64, 'Uint64'); + _ws.data.setUint64(position, value, endian); } - /// Returns a view of the written bytes (from index 0 up to the current - /// [bytesWritten]) without resetting the writer. + /// Writes a 32-bit floating-point number at the specified [position] without + /// changing the current write position. /// - /// Unlike [takeBytes], this does not reset the writer's state. - /// Subsequent writes will continue appending to the buffer. + /// [endian] specifies byte order (defaults to big-endian). /// - /// **Note:** Since this returns a view, the content of the returned list - /// will change if you continue writing to this writer. + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 3]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setFloat32(int position, double value, [Endian endian = Endian.big]) { + if (position < 0 || position + 4 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 4, 'position'); + } + _ws.data.setFloat32(position, value, endian); + } + + /// Writes a 64-bit floating-point number at the specified [position] without + /// changing the current write position. /// - /// **Use case:** When you need to inspect or copy data mid-stream. + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Throws [RangeError] if [position] is negative or beyond + /// [bytesWritten - 7]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void setFloat64(int position, double value, [Endian endian = Endian.big]) { + if (position < 0 || position + 8 > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset - 8, 'position'); + } + _ws.data.setFloat64(position, value, endian); + } + + /// Writes a byte at the specified [position] without changing the current + /// write position. + /// + /// Used to overwrite data at a previously written offset (e.g., + /// updating a length field). + /// + /// This is a functional alias for `operator []=`. + /// + /// Throws [RangeError] if [position] is negative or greater than or equal to + /// [bytesWritten]. /// /// Example: /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint32(42); - /// final snapshot = writer.toBytes(); // Peek at current data - /// writer.writeUint32(100); // Continue writing - /// final final = writer.takeBytes(); // Get all data + /// writer.writeUint32(10); // Write length placeholder + /// writer.writeString('data'); + /// writer.writeUint8At(0, 15); // Overwrite length at position 0 + /// // or: writer[0] = 15; /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); + void writeUint8At(int position, int value) => this[position] = value; +} +/// Position management methods for [BinaryWriter]. +extension BinaryWriterPosition on BinaryWriter { /// Sets the write position to the specified byte offset. /// /// Subsequent writes will start from this new position. @@ -898,66 +1247,35 @@ extension type BinaryWriter._(_WriterState _ws) { _ws.offset = target + length; } } +} - /// Returns the byte at the specified [index] without changing the current - /// write position. - /// - /// Throws [RangeError] if [index] is negative or greater than or equal to - /// [bytesWritten]. +/// Internal methods for [BinaryWriter]. +extension _BinaryWriterInternal on BinaryWriter { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - int operator [](int index) { - if (index < 0 || index >= _ws.offset) { - throw RangeError.range(index, 0, _ws.offset - 1, 'index'); - } - return _ws.list[index]; - } + int _varIntSize(int value) => switch (value) { + < 0x80 => 1, + < 0x4000 => 2, + < 0x200000 => 3, + < 0x10000000 => 4, + _ => 5, + }; - /// Writes a byte at the specified [index] without changing the current - /// write position. - /// - /// This operator is used to overwrite already written bytes. To append data, - /// use the standard `write*` methods. - /// - /// Throws [RangeError] if [index] is negative or greater than or equal to - /// [bytesWritten]. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void operator []=(int index, int value) { - if (index < 0 || index >= _ws.offset) { - throw RangeError.range(index, 0, _ws.offset - 1, 'index'); + void _writeLength(int length, LengthEncoding encoding) { + switch (encoding) { + case .u8: + writeUint8(length); + case .u16: + writeUint16(length); + case .u32: + writeUint32(length); + case .u64: + writeUint64(length); } - - _checkRange(value, 0, 255, 'Uint8'); - _ws.list[index] = value; } - /// Writes a byte at the specified [position] without changing the current - /// write position. - /// - /// Used to overwrite data at a previously written offset (e.g., - /// updating a length field). - /// - /// This is a functional alias for `operator []=`. - /// - /// Throws [RangeError] if [position] is negative or greater than or equal to - /// [bytesWritten]. - /// - /// Example: - /// ```dart - /// writer.writeUint32(10); // Write length placeholder - /// writer.writeString('data'); - /// writer.writeUint8At(0, 15); // Overwrite length at position 0 - /// // or: writer[0] = 15; - /// ``` - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void writeUint8At(int position, int value) => this[position] = value; - - /// Resets the writer to its initial state, discarding all written data. - @pragma('vm:prefer-inline') - void reset() => _ws._initializeBuffer(); - /// Handles malformed UTF-16 sequences (lone surrogates). /// /// If [allow] is false, throws [FormatException]. diff --git a/pubspec.yaml b/pubspec.yaml index 8a127d2..2948991 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pro_binary description: Efficient binary serialization library for Dart. Encodes and decodes various data types. -version: 5.2.0 +version: 5.3.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues diff --git a/test/unit/binary_reader_get_test.dart b/test/unit/binary_reader_get_test.dart new file mode 100644 index 0000000..5096258 --- /dev/null +++ b/test/unit/binary_reader_get_test.dart @@ -0,0 +1,410 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Get Random Access', () { + late BinaryReader reader; + + setUp(() { + // 32 bytes to support all tests including 64-bit reads + final buffer = Uint8List.fromList(List.generate(32, (i) => i)); + reader = BinaryReader(buffer); + }); + + group('getUint8', () { + test('reads byte at position', () { + expect(reader.getUint8(0), equals(0x00)); + expect(reader.getUint8(5), equals(0x05)); + expect(reader.getUint8(31), equals(0x1F)); + }); + + test('does not change offset', () { + reader + ..seek(5) + ..getUint8(0); + expect(reader.offset, equals(5)); + }); + + test('throws for negative position', () { + expect(() => reader.getUint8(-1), throwsA(isA())); + }); + + test('throws for position at end', () { + expect(() => reader.getUint8(32), throwsA(isA())); + }); + }); + + group('getInt16 / getUint16', () { + test('getUint16 big-endian round-trip', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + reader = BinaryReader(buffer); + expect(reader.getUint16(0), equals(0x0102)); + expect(reader.getUint16(2), equals(0x0304)); + expect(reader.getUint16(4), equals(0x0506)); + }); + + test('getUint16 little-endian round-trip', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + reader = BinaryReader(buffer); + expect(reader.getUint16(0, Endian.little), equals(0x0201)); + expect(reader.getUint16(2, Endian.little), equals(0x0403)); + }); + + test('getInt16 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0xFF, 0xFF, // -1 + 0x7F, 0xFF, // 32767 + 0x80, 0x00, // -32768 + ]); + reader = BinaryReader(buffer); + expect(reader.getInt16(0), equals(-1)); + expect(reader.getInt16(2), equals(32767)); + expect(reader.getInt16(4), equals(-32768)); + }); + + test('getUint16 does not change offset', () { + reader + ..seek(5) + ..getUint16(0); + expect(reader.offset, equals(5)); + }); + + test('throws for position out of bounds', () { + // 32-byte buffer: getUint16(31) needs bytes 31-32, but 32 is out of + // bounds + expect(() => reader.getUint16(31), throwsA(isA())); + }); + + test('throws for negative position', () { + expect(() => reader.getUint16(-1), throwsA(isA())); + }); + }); + + group('getInt32 / getUint32', () { + test('getUint32 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, 0x00, 0x00, 0x01, // 1 + 0x00, 0x01, 0x00, 0x00, // 65536 + 0x01, 0x00, 0x00, 0x00, // 16777216 + ]); + reader = BinaryReader(buffer); + expect(reader.getUint32(0), equals(1)); + expect(reader.getUint32(4), equals(65536)); + expect(reader.getUint32(8), equals(16777216)); + }); + + test('getUint32 little-endian round-trip', () { + final buffer = Uint8List.fromList([0x01, 0x00, 0x00, 0x00]); + reader = BinaryReader(buffer); + expect(reader.getUint32(0, Endian.little), equals(1)); + }); + + test('getInt32 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, 0x00, 0x01, 0x00, // 256 + 0x7F, 0xFF, 0xFF, 0xFF, // max int32 + 0xFF, 0xFF, 0xFF, 0xFF, // -1 + ]); + reader = BinaryReader(buffer); + expect(reader.getInt32(0), equals(256)); + expect(reader.getInt32(4), equals(2147483647)); + expect(reader.getInt32(8), equals(-1)); + }); + + test('getInt32 little-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, 0x00, 0x01, 0x00, // 65536 in little-endian + ]); + reader = BinaryReader(buffer); + expect(reader.getInt32(0, Endian.little), equals(65536)); + }); + + test('getUint32 does not change offset', () { + reader + ..seek(10) + ..getUint32(0); + expect(reader.offset, equals(10)); + }); + + test('throws for position out of bounds', () { + // 32-byte buffer: getUint32(29) needs bytes 29-32, but 32 is out of + // bounds + expect(() => reader.getUint32(29), throwsA(isA())); + }); + + test('throws for negative position', () { + expect(() => reader.getUint32(-1), throwsA(isA())); + }); + }); + + group('getInt64 / getUint64', () { + test('getUint64 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + ]); + reader = BinaryReader(buffer); + expect(reader.getUint64(0), equals(1)); + expect(reader.getUint64(8), equals(65536)); + }); + + test('getUint64 little-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + reader = BinaryReader(buffer); + expect(reader.getUint64(0, Endian.little), equals(1)); + }); + + test('getInt64 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, // 256 + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // max int64 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // -1 + ]); + reader = BinaryReader(buffer); + expect(reader.getInt64(0), equals(256)); + expect(reader.getInt64(8), equals(9223372036854775807)); + expect(reader.getInt64(16), equals(-1)); + }); + + test('getInt64 little-endian round-trip', () { + final buffer = Uint8List.fromList([ + // 65536 in little-endian + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + reader = BinaryReader(buffer); + expect(reader.getInt64(0, Endian.little), equals(65536)); + }); + + test('getUint64 does not change offset', () { + reader + ..seek(10) + ..getUint64(0); + expect(reader.offset, equals(10)); + }); + + test('throws for position out of bounds', () { + // 32-byte buffer: getUint64(25) needs bytes 25-32, but 32 is out of + // bounds + expect(() => reader.getUint64(25), throwsA(isA())); + }); + + test('throws for negative position', () { + expect(() => reader.getUint64(-1), throwsA(isA())); + }); + }); + + group('getFloat32 / getFloat64', () { + test('getFloat32 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x40, 0x48, 0x00, 0x00, // 3.125 + 0xC0, 0x00, 0x00, 0x00, // -2.0 + 0x00, 0x00, 0x00, 0x00, // 0.0 + ]); + reader = BinaryReader(buffer); + expect(reader.getFloat32(0), closeTo(3.125, 0.001)); + expect(reader.getFloat32(4), closeTo(-2.0, 0.001)); + expect(reader.getFloat32(8), equals(0.0)); + }); + + test('getFloat32 little-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x00, 0x00, 0x48, 0x40, // 3.125 in little-endian + ]); + reader = BinaryReader(buffer); + expect(reader.getFloat32(0, Endian.little), closeTo(3.125, 0.001)); + }); + + test('getFloat64 big-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E, // 3.14159 + 0xC0, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E, // -3.14159 + ]); + reader = BinaryReader(buffer); + expect(reader.getFloat64(0), closeTo(3.14159, 0.00001)); + expect(reader.getFloat64(8), closeTo(-3.14159, 0.00001)); + }); + + test('getFloat64 little-endian round-trip', () { + final buffer = Uint8List.fromList([ + 0x6E, + 0x86, + 0x1B, + 0xF0, + 0xF9, + 0x21, + 0x09, + 0x40, // 3.14159 little-endian + ]); + reader = BinaryReader(buffer); + expect(reader.getFloat64(0, Endian.little), closeTo(3.14159, 0.00001)); + }); + + test('getFloat32 does not change offset', () { + reader + ..seek(10) + ..getFloat32(0); + expect(reader.offset, equals(10)); + }); + + test('getFloat64 does not change offset', () { + reader + ..seek(10) + ..getFloat64(0); + expect(reader.offset, equals(10)); + }); + + test('throws for position out of bounds', () { + // 32-byte buffer + expect(() => reader.getFloat32(29), throwsA(isA())); + expect(() => reader.getFloat64(25), throwsA(isA())); + }); + + test('throws for negative position', () { + expect(() => reader.getFloat32(-1), throwsA(isA())); + expect(() => reader.getFloat64(-1), throwsA(isA())); + }); + }); + + group('use case: parsing header fields', () { + test('read fixed-size header without consuming', () { + // Simulate a packet: [version(1), type(1), length(4), payload...] + final buffer = Uint8List.fromList([ + 0x02, // version 2 + 0x05, // type = 5 + 0x00, 0x00, 0x00, 0x0A, // length = 10 + 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" + 0x20, 0x57, 0x6F, 0x72, 0x6C, // " Worl" + 0x64, 0x21, // "d!" + ]); + reader = BinaryReader(buffer); + + final version = reader.getUint8(0); + final type = reader.getUint8(1); + final length = reader.getUint32(2); + + expect(version, equals(2)); + expect(type, equals(5)); + expect(length, equals(10)); + expect(reader.offset, equals(0)); + }); + + test('read mixed-endian header', () { + // Network byte order (big-endian) for most fields + final buffer = Uint8List.fromList([ + 0x00, 0x01, // sequence number (big-endian) + 0x00, 0x00, 0x00, 0x64, // length (big-endian) + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, // timestamp (big-endian) + ]); + reader = BinaryReader(buffer); + + expect(reader.getUint16(0), equals(1)); + expect(reader.getUint32(2), equals(100)); + expect(reader.getUint64(6), equals(256)); + }); + + test('read floating-point metadata', () { + final buffer = Uint8List.fromList([ + 0x40, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E, // lat: 3.14159 + 0xC0, 0x09, 0x21, 0xF9, 0xF0, 0x1B, 0x86, 0x6E, // lon: -3.14159 + 0x41, 0x20, 0x00, 0x00, // altitude: 10.0 + ]); + reader = BinaryReader(buffer); + + expect(reader.getFloat64(0), closeTo(3.14159, 0.00001)); + expect(reader.getFloat64(8), closeTo(-3.14159, 0.00001)); + expect(reader.getFloat32(16), closeTo(10.0, 0.001)); + }); + }); + + group('edge cases', () { + test('read from empty buffer', () { + final emptyBuffer = Uint8List(0); + final emptyReader = BinaryReader(emptyBuffer); + expect(() => emptyReader.getUint8(0), throwsA(isA())); + }); + + test('read from single byte buffer', () { + final buffer = Uint8List.fromList([0x42]); + final reader = BinaryReader(buffer); + expect(reader.getUint8(0), equals(0x42)); + expect(() => reader.getUint16(0), throwsA(isA())); + }); + + test('multiple reads at same position', () { + final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03]); + reader = BinaryReader(buffer); + expect(reader.getUint16(0), equals(0x0001)); + expect(reader.getUint16(0), equals(0x0001)); + expect(reader.getUint16(0), equals(0x0001)); + }); + + test('interleaved reads at different positions', () { + final buffer = Uint8List.fromList([ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + ]); + reader = BinaryReader(buffer); + expect(reader.getUint16(0), equals(0x0001)); + expect(reader.getUint32(2), equals(0x02030405)); + expect(reader.getUint16(6), equals(0x0607)); + expect(reader.getUint16(0), equals(0x0001)); + }); + + test('works after partial read', () { + final buffer = Uint8List.fromList([ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + ]); + reader = BinaryReader(buffer) + ..readUint16(); // reads 2 bytes, offset = 2 + expect(reader.getUint32(0), equals(0x00010203)); + expect(reader.getUint32(4), equals(0x04050607)); + expect(reader.offset, equals(2)); + }); + }); + }); +} diff --git a/test/unit/binary_writer_set_get_test.dart b/test/unit/binary_writer_set_get_test.dart new file mode 100644 index 0000000..a2a3daa --- /dev/null +++ b/test/unit/binary_writer_set_get_test.dart @@ -0,0 +1,449 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Set/Get', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + group('getUint8', () { + test('reads byte at position', () { + writer + ..writeUint8(10) + ..writeUint8(20) + ..writeUint8(30); + expect(writer.getUint8(0), equals(10)); + expect(writer.getUint8(1), equals(20)); + expect(writer.getUint8(2), equals(30)); + }); + + test('does not change offset', () { + writer + ..writeUint8(42) + ..writeUint8(99); + final offset = writer.bytesWritten; + writer.getUint8(0); + expect(writer.bytesWritten, equals(offset)); + }); + + test('throws for negative position', () { + writer.writeUint8(1); + expect(() => writer.getUint8(-1), throwsA(isA())); + }); + + test('throws for position at end', () { + writer.writeUint8(1); + expect(() => writer.getUint8(1), throwsA(isA())); + }); + }); + + group('setUint8', () { + test('overwrites byte at position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3) + ..setUint8(1, 99); + expect(writer.toBytes(), equals([1, 99, 3])); + }); + + test('does not change offset', () { + writer + ..writeUint8(1) + ..writeUint8(2); + final offset = writer.bytesWritten; + writer.setUint8(0, 99); + expect(writer.bytesWritten, equals(offset)); + }); + + test('throws for negative position', () { + writer.writeUint8(1); + expect(() => writer.setUint8(-1, 1), throwsRangeError); + }); + + test('throws for position at end', () { + writer.writeUint8(1); + expect(() => writer.setUint8(1, 1), throwsRangeError); + }); + + test('throws for value out of range', () { + writer + ..writeUint8(1) + ..writeUint8(2); + expect(() => writer.setUint8(0, 256), throwsRangeError); + expect(() => writer.setUint8(0, -1), throwsRangeError); + }); + + test('accepts boundary values', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..setUint8(0, 0) + ..setUint8(1, 255); + expect(writer.toBytes(), equals([0, 255])); + }); + }); + + group('getInt16 / setInt16', () { + test('round-trip big-endian', () { + writer + ..writeInt16(-100) + ..writeInt16(32767) + ..writeInt16(-32768); + expect(writer.getInt16(0), equals(-100)); + expect(writer.getInt16(2), equals(32767)); + expect(writer.getInt16(4), equals(-32768)); + }); + + test('round-trip little-endian', () { + writer + ..writeInt16(-100, Endian.little) + ..writeInt16(32767, Endian.little); + expect(writer.getInt16(0, Endian.little), equals(-100)); + expect(writer.getInt16(2, Endian.little), equals(32767)); + }); + + test('setInt16 overwrites without changing offset', () { + writer + ..writeInt16(100) + ..writeInt16(200) + ..setInt16(0, 999); + expect(writer.getInt16(0), equals(999)); + expect(writer.getInt16(2), equals(200)); + expect(writer.bytesWritten, equals(4)); + }); + + test('throws for position out of bounds', () { + writer.writeInt16(100); + expect(() => writer.getInt16(1), throwsA(isA())); + expect(() => writer.setInt16(1, 100), throwsRangeError); + }); + + test('throws for negative position', () { + writer.writeInt16(100); + expect(() => writer.getInt16(-1), throwsA(isA())); + expect(() => writer.setInt16(-1, 100), throwsRangeError); + }); + + test('throws for value out of range', () { + writer + ..writeInt16(1) + ..writeInt16(2); + expect(() => writer.setInt16(0, 32768), throwsRangeError); + expect(() => writer.setInt16(0, -32769), throwsRangeError); + }); + }); + + group('getUint16 / setUint16', () { + test('round-trip big-endian', () { + writer + ..writeUint16(0) + ..writeUint16(256) + ..writeUint16(65535); + expect(writer.getUint16(0), equals(0)); + expect(writer.getUint16(2), equals(256)); + expect(writer.getUint16(4), equals(65535)); + }); + + test('setUint16 overwrites without changing offset', () { + writer + ..writeUint16(100) + ..writeUint16(200) + ..setUint16(0, 999); + expect(writer.getUint16(0), equals(999)); + expect(writer.bytesWritten, equals(4)); + }); + + test('throws for value out of range', () { + writer + ..writeUint16(1) + ..writeUint16(2); + expect(() => writer.setUint16(0, 65536), throwsRangeError); + expect(() => writer.setUint16(0, -1), throwsRangeError); + }); + }); + + group('getInt32 / setInt32', () { + test('round-trip big-endian', () { + writer + ..writeInt32(-500000) + ..writeInt32(2147483647) + ..writeInt32(-2147483648); + expect(writer.getInt32(0), equals(-500000)); + expect(writer.getInt32(4), equals(2147483647)); + expect(writer.getInt32(8), equals(-2147483648)); + }); + + test('round-trip little-endian', () { + writer.writeInt32(-500000, Endian.little); + expect(writer.getInt32(0, Endian.little), equals(-500000)); + }); + + test('setInt32 overwrites without changing offset', () { + writer + ..writeInt32(100) + ..writeInt32(200) + ..setInt32(0, 999); + expect(writer.getInt32(0), equals(999)); + expect(writer.getInt32(4), equals(200)); + expect(writer.bytesWritten, equals(8)); + }); + + test('throws for position out of bounds', () { + writer.writeInt32(100); + expect(() => writer.getInt32(3), throwsA(isA())); + expect(() => writer.setInt32(3, 100), throwsRangeError); + }); + + test('throws for value out of range', () { + writer + ..writeInt32(1) + ..writeInt32(2); + expect(() => writer.setInt32(0, 2147483648), throwsRangeError); + expect(() => writer.setInt32(0, -2147483649), throwsRangeError); + }); + }); + + group('getUint32 / setUint32', () { + test('round-trip big-endian', () { + writer + ..writeUint32(0) + ..writeUint32(16777216) + ..writeUint32(4294967295); + expect(writer.getUint32(0), equals(0)); + expect(writer.getUint32(4), equals(16777216)); + expect(writer.getUint32(8), equals(4294967295)); + }); + + test('setUint32 overwrites without changing offset', () { + writer + ..writeUint32(100) + ..writeUint32(200) + ..setUint32(0, 999); + expect(writer.getUint32(0), equals(999)); + expect(writer.bytesWritten, equals(8)); + }); + + test('throws for value out of range', () { + writer + ..writeUint32(1) + ..writeUint32(2); + expect(() => writer.setUint32(0, 4294967296), throwsRangeError); + expect(() => writer.setUint32(0, -1), throwsRangeError); + }); + }); + + group('getInt64 / setInt64', () { + test('round-trip big-endian', () { + writer + ..writeInt64(-1234567890123456) + ..writeInt64(9223372036854775807) + ..writeInt64(-9223372036854775808); + expect(writer.getInt64(0), equals(-1234567890123456)); + expect(writer.getInt64(8), equals(9223372036854775807)); + expect(writer.getInt64(16), equals(-9223372036854775808)); + }); + + test('round-trip little-endian', () { + writer.writeInt64(-1234567890123456, Endian.little); + expect(writer.getInt64(0, Endian.little), equals(-1234567890123456)); + }); + + test('setInt64 overwrites without changing offset', () { + writer + ..writeInt64(100) + ..writeInt64(200) + ..setInt64(0, 999); + expect(writer.getInt64(0), equals(999)); + expect(writer.getInt64(8), equals(200)); + expect(writer.bytesWritten, equals(16)); + }); + + test('throws for position out of bounds', () { + writer.writeInt64(100); + expect(() => writer.getInt64(7), throwsA(isA())); + expect(() => writer.setInt64(7, 100), throwsRangeError); + }); + + test('throws for value out of range', () { + writer + ..writeInt64(1) + ..writeInt64(2) + // kMaxInt64 + 1 and kMinInt64 - 1 cannot be represented as Dart int + // Verify setInt64 works correctly with valid values + ..setInt64(0, 9223372036854775807) + ..setInt64(8, -9223372036854775808); + expect(writer.getInt64(0), equals(9223372036854775807)); + expect(writer.getInt64(8), equals(-9223372036854775808)); + }); + }); + + group('getUint64 / setUint64', () { + test('round-trip big-endian', () { + writer + ..writeUint64(0) + ..writeUint64(9223372036854775807); + expect(writer.getUint64(0), equals(0)); + expect(writer.getUint64(8), equals(9223372036854775807)); + }); + + test('setUint64 overwrites without changing offset', () { + writer + ..writeUint64(100) + ..writeUint64(200) + ..setUint64(0, 999); + expect(writer.getUint64(0), equals(999)); + expect(writer.bytesWritten, equals(16)); + }); + + test('sets boundary values correctly', () { + writer + ..writeUint64(0) + ..writeUint64(9223372036854775807) + ..setUint64(0, 9223372036854775807); + expect(writer.getUint64(0), equals(9223372036854775807)); + }); + }); + + group('getFloat32 / setFloat32', () { + test('round-trip big-endian', () { + writer + ..writeFloat32(3.14) + ..writeFloat32(-2.5) + ..writeFloat32(0); + expect(writer.getFloat32(0), closeTo(3.14, 0.001)); + expect(writer.getFloat32(4), closeTo(-2.5, 0.001)); + expect(writer.getFloat32(8), equals(0.0)); + }); + + test('round-trip little-endian', () { + writer.writeFloat32(3.14159, Endian.little); + expect(writer.getFloat32(0, Endian.little), closeTo(3.14159, 0.0001)); + }); + + test('setFloat32 overwrites without changing offset', () { + writer + ..writeFloat32(1.5) + ..writeFloat32(2.5) + ..setFloat32(0, 9.9); + expect(writer.getFloat32(0), closeTo(9.9, 0.001)); + expect(writer.getFloat32(4), closeTo(2.5, 0.001)); + expect(writer.bytesWritten, equals(8)); + }); + + test('throws for position out of bounds', () { + writer.writeFloat32(1); + expect(() => writer.getFloat32(3), throwsA(isA())); + expect(() => writer.setFloat32(3, 1), throwsRangeError); + }); + }); + + group('getFloat64 / setFloat64', () { + test('round-trip big-endian', () { + writer + ..writeFloat64(3.14159265358979) + ..writeFloat64(-2.5); + expect( + writer.getFloat64(0), + closeTo(3.14159265358979, 0.00000000000001), + ); + expect(writer.getFloat64(8), closeTo(-2.5, 0.00000000000001)); + }); + + test('round-trip little-endian', () { + writer.writeFloat64(3.14159265358979, Endian.little); + expect( + writer.getFloat64(0, Endian.little), + closeTo(3.14159265358979, 0.00000000000001), + ); + }); + + test('setFloat64 overwrites without changing offset', () { + writer + ..writeFloat64(1.5) + ..writeFloat64(2.5) + ..setFloat64(0, 9.9); + expect(writer.getFloat64(0), closeTo(9.9, 0.00000000000001)); + expect(writer.getFloat64(8), closeTo(2.5, 0.00000000000001)); + expect(writer.bytesWritten, equals(16)); + }); + + test('throws for position out of bounds', () { + writer.writeFloat64(1); + expect(() => writer.getFloat64(7), throwsA(isA())); + expect(() => writer.setFloat64(7, 1), throwsRangeError); + }); + }); + + group('use case: backpatch pattern', () { + test('setUint32 for length prefix', () { + final data = [1, 2, 3, 4, 5]; + writer + ..writeUint32(0) // placeholder for length + ..writeBytes(data) + ..setUint32(0, data.length); + + expect(writer.getUint32(0), equals(data.length)); + expect(writer.toBytes().sublist(4), equals(data)); + }); + + test('setInt32 for message type', () { + writer + ..writeUint32(0) // placeholder for type + ..writeUint32(42) // payload + ..writeUint32(100) // more payload + ..setInt32(0, 5); // type = 5 + + expect(writer.getInt32(0), equals(5)); + expect(writer.getInt32(4), equals(42)); + expect(writer.getInt32(8), equals(100)); + }); + + test('setFloat64 for header field', () { + writer + ..writeFloat64(0) // placeholder + ..writeUint32(12345) + ..writeString('hello') + ..setFloat64(0, 3.14); + + expect(writer.getFloat64(0), closeTo(3.14, 0.00000000000001)); + expect(writer.getUint32(8), equals(12345)); + }); + }); + + group('edge cases', () { + test('get/set at position 0 after single write', () { + writer.writeUint8(42); + expect(writer.getUint8(0), equals(42)); + writer.setUint8(0, 99); + expect(writer.getUint8(0), equals(99)); + }); + + test('multiple get/set at same position', () { + writer + ..writeUint32(100) + ..writeUint32(200); + expect(writer.getUint32(0), equals(100)); + expect(writer.getUint32(0), equals(100)); + writer.setUint32(0, 300); + expect(writer.getUint32(0), equals(300)); + expect(writer.bytesWritten, equals(8)); + }); + + test('get/set with different endianness', () { + writer + ..writeUint16(0x1234, Endian.little) + ..writeUint16(0x5678); + expect(writer.getUint16(0, Endian.little), equals(0x1234)); + expect(writer.getUint16(2), equals(0x5678)); + writer.setUint16(0, 0xABCD, Endian.little); + expect(writer.getUint16(0, Endian.little), equals(0xABCD)); + }); + }); + }); +} From 01e5425f5ebb4ea180a76f5595e3df8463a9721c Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 31 May 2026 22:54:20 +0300 Subject: [PATCH 2/5] performance improvements --- lib/src/stream/stream_binary_reader.dart | 73 +++++++++++++++++------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index e59f0e1..ae5167f 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -351,8 +351,31 @@ extension type StreamBinaryReader._(_StreamReaderState _s) T _readCrossChunk(int length, T Function(ByteData) parser) { if (length <= 8) { final scratch = _s.scratchBuffer; - for (var i = 0; i < length; i++) { - scratch[i] = readUint8(); + var remaining = length; + var scratchOffset = 0; + + while (remaining > 0) { + final cr = _s.currentReader!; + final chunkAvailable = cr.availableBytes; + final readLen = chunkAvailable >= remaining + ? remaining + : chunkAvailable; + + if (readLen > 0) { + final chunk = _s.currentChunk!; + var offset = cr.offset; + for (var i = 0; i < readLen; i++) { + scratch[scratchOffset++] = chunk[offset++]; + } + + cr.skip(readLen); + _s.availableBytes -= readLen; + remaining -= readLen; + } + + if (cr.availableBytes == 0) { + _advanceChunk(); + } } return parser(_s.scratchData); @@ -388,7 +411,17 @@ extension type StreamBinaryReader._(_StreamReaderState _s) var result = 0; var shift = 0; for (var i = 0; i < 10; i++) { - final byte = readUint8(); + if (_s.availableBytes == 0) { + throw const NotEnoughDataException(1, 0); + } + final cr = _s.currentReader!; + final byte = _s.currentChunk![cr.offset]; + cr.skip(1); + _s.availableBytes -= 1; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + result |= (byte & 0x7f) << shift; if ((byte & 0x80) == 0) { return result; @@ -448,27 +481,24 @@ extension type StreamBinaryReader._(_StreamReaderState _s) while (remaining > 0) { final chunkReader = _s.currentReader!; final chunkAvailable = chunkReader.availableBytes; + final readLen = chunkAvailable >= remaining ? remaining : chunkAvailable; - if (chunkAvailable >= remaining) { - final bytes = chunkReader.readBytes(remaining); - result.setRange(resultOffset, resultOffset + remaining, bytes); + if (readLen > 0) { + result.setRange( + resultOffset, + resultOffset + readLen, + _s.currentChunk!, + chunkReader.offset, + ); - _s.availableBytes -= remaining; + chunkReader.skip(readLen); + _s.availableBytes -= readLen; - if (chunkReader.availableBytes == 0) { - _advanceChunk(); - } - break; - } else { - if (chunkAvailable > 0) { - final bytes = chunkReader.readBytes(chunkAvailable); - result.setRange(resultOffset, resultOffset + chunkAvailable, bytes); - - resultOffset += chunkAvailable; - remaining -= chunkAvailable; - _s.availableBytes -= chunkAvailable; - } + resultOffset += readLen; + remaining -= readLen; + } + if (chunkReader.availableBytes == 0) { _advanceChunk(); } } @@ -619,6 +649,7 @@ final class _StreamReaderState extends ChunkedTransactionalState } BinaryReader? currentReader; + Uint8List? currentChunk; /// Pre-allocated buffer for zero-allocation cross-chunk primitive reads. final Uint8List scratchBuffer; @@ -647,6 +678,7 @@ final class _StreamReaderState extends ChunkedTransactionalState @override void onBindReader(Uint8List chunk, int offset) { + currentChunk = chunk; final cr = currentReader; if (cr != null) { cr @@ -659,6 +691,7 @@ final class _StreamReaderState extends ChunkedTransactionalState @override void onUnbindReader() { + currentChunk = null; currentReader = null; } From 31669b4e63ffdebfa96b5e5e6828706a6efabd31 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 31 May 2026 22:54:32 +0300 Subject: [PATCH 3/5] update readme --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8575ad8..500845a 100644 --- a/README.md +++ b/README.md @@ -6,35 +6,56 @@ **High-performance binary serialization and deserialization for Dart.** Optimized for high-frequency network protocols, real-time streaming, and fast local storage. Features zero-copy reads, object pooling, and transactional stream parsing. +## Table of Contents + +- [pro\_binary](#pro_binary) + - [Table of Contents](#table-of-contents) + - [Key Features](#key-features) + - [Installation](#installation) + - [Quick Start](#quick-start) + - [Recipes \& Patterns](#recipes--patterns) + - [1. Efficient Object Serialization](#1-efficient-object-serialization) + - [2. High-Frequency writes (Pooling)](#2-high-frequency-writes-pooling) + - [3. Stream Parsing (Async Binary Messages)](#3-stream-parsing-async-binary-messages) + - [4. Binary Packets (Manual navigation)](#4-binary-packets-manual-navigation) + - [Examples](#examples) + - [API Overview](#api-overview) + - [Performance](#performance) + - [String Encoding (One-Pass vs `utf8.encode`)](#string-encoding-one-pass-vs-utf8encode) + - [Object Serialization \& Deserialization](#object-serialization--deserialization) + - [Object Pooling](#object-pooling) + - [Testing](#testing) + - [Contributing](#contributing) + - [License](#license) + ## Key Features -* **Extreme Performance:** Built from the ground up for speed. Leverages Dart Extension Types for zero-overhead abstractions and direct memory manipulation. -* **Zero-Copy Reads:** Deserialization operations return `Uint8List` views instead of allocating new memory arrays, significantly reducing GC (Garbage Collector) pauses. -* **One-Pass String Encoding:** Features a highly optimized `writeVarString` with optimistic size estimation and native memory shifting. Up to **~30% faster** than standard `utf8.encode`. -* **Zero-Allocation Object Pooling:** Includes built-in `BinaryWriterPool` to reuse writer instances. Perfect for high-frequency network packets (e.g., game servers, WebSockets). -* **Compact Encoding:** Native support for VarInt and ZigZag encoding to shrink payload sizes for integers. -* **Transactional Stream Parsing:** Easily process fragmented asynchronous data chunks using `StreamBinaryReader` with `bookmark()` and `rollback()` capabilities. -* **Cross-Platform:** 100% pure Dart. Works seamlessly across Native (AOT/JIT) and Web (WASM/JS) with a consistent, predictable API. +- **Extreme Performance:** Built from the ground up for speed. Leverages Dart Extension Types for zero-overhead abstractions and direct memory manipulation. +- **Zero-Copy Reads:** Deserialization operations return `Uint8List` views instead of allocating new memory arrays, significantly reducing GC (Garbage Collector) pauses. +- **One-Pass String Encoding:** Features a highly optimized `writeVarString` with optimistic size estimation and native memory shifting. Up to **~30% faster** than standard `utf8.encode`. +- **Zero-Allocation Object Pooling:** Includes built-in `BinaryWriterPool` to reuse writer instances. Perfect for high-frequency network packets (e.g., game servers, WebSockets). +- **Compact Encoding:** Native support for VarInt and ZigZag encoding to shrink payload sizes for integers. +- **Transactional Stream Parsing:** Easily process fragmented asynchronous data chunks using `StreamBinaryReader` with `bookmark()` and `rollback()` capabilities. +- **Cross-Platform:** 100% pure Dart. Works seamlessly across Native (AOT/JIT) and Web (WASM/JS) with a consistent, predictable API. ## Installation +Add `pro_binary` to your `pubspec.yaml` manually: + ```yaml dependencies: - pro_binary: last_version + pro_binary: ``` -or: - -Add `pro_binary` as a dependency to your project: +Or add it using the command line: ```bash +# For Dart projects dart pub add pro_binary -``` - -Or for Flutter projects: -```bash +# For Flutter projects flutter pub add pro_binary +``` ## Quick Start @@ -47,7 +68,7 @@ final writer = BinaryWriter() ..writeVarString('Dart 🚀') ..writeBool(true); -final bytes = writer.takeBytes(); +final bytes = writer.takeBytes(); // takes the buffer and resets the writer // Deserialize final reader = BinaryReader(bytes); @@ -72,8 +93,8 @@ class User { User(this.id, this.name); void encode(BinaryWriter w) => w - ..writeVarUint(id) - ..writeVarString(name); + ..writeVarUint(id) // compact integer encoding + ..writeVarString(name); // fast one-pass UTF-8 encoding factory User.decode(BinaryReader r) => User( @@ -173,9 +194,9 @@ if (reader.hasBytes(4)) { Explore the [example](example/) directory for complete, runnable projects: -* [Basic Usage](example/basic/): Simple serialization and deserialization. -* [File Streaming](example/file_streaming/): Reading and writing large binary files using streams. -* [Network Streaming](example/network_streaming/): Implementing a custom protocol for TCP/Socket data. +- [Basic Usage](example/basic/): Simple serialization and deserialization. +- [File Streaming](example/file_streaming/): Reading and writing large binary files using streams. +- [Network Streaming](example/network_streaming/): Implementing a custom protocol for TCP/Socket data. ## API Overview @@ -188,12 +209,37 @@ Explore the [example](example/) directory for complete, runnable projects: | **StreamBinaryReader** | Handles async data chunks seamlessly with a transactional `bookmark`/`rollback` model for partial data. | | **BinaryStreamTransformer** | The easiest way to parse a `Stream>` into a stream of typed messages or objects. | | **BinaryWriterPool** | Object pool for `BinaryWriter` to eliminate GC pressure during high-frequency write operations. | -| **getUtf8Length** | High-speed utility to calculate UTF-8 byte length without encoding (O(n) but heavily optimized). | -| **TransactionalReader** | Base interface for custom transactional readers. Used internally by `StreamBinaryReader`. | ## Performance -Run benchmarks to see it in action: +`pro_binary` is built for extreme performance. Our AOT benchmarks show massive improvements over standard Dart approaches: + +### String Encoding (One-Pass vs `utf8.encode`) + +Our highly optimized one-pass string encoder is **up to 2.7x faster** than standard `utf8.encode`. + +| Payload | `pro_binary` (One-Pass) | Standard (`utf8.encode`) | Speedup | +| :--- | :--- | :--- | :--- | +| **ASCII** | 0.79 μs | 2.15 μs | **2.7x** | +| **Mixed UTF-8** | 1.15 μs | 2.62 μs | **2.28x** | +| **Emoji / Complex** | 1.91 μs | 4.17 μs | **2.18x** | + +### Object Serialization & Deserialization + +Extremely low overhead for serializing and deserializing Dart objects. + +| Scenario | Serialization | Deserialization | +| :--- | :--- | :--- | +| **Simple Message** | 0.31 μs | 0.14 μs | +| **Complex Profile** | 1.62 μs | 1.73 μs | +| **10K integers array** | 403.5 μs | 284.5 μs | + +### Object Pooling + +Using `BinaryWriterPool` reduces allocation overhead and virtually eliminates GC (Garbage Collector) pauses during high-frequency writes (like game servers or real-time trading). + +--- +Run these benchmarks yourself to see it in action: ```bash # Serialization (Writer) @@ -221,6 +267,21 @@ dart test dart test --coverage=coverage ``` +## Contributing + +Contributions are welcome! Please ensure that all tests pass and code is formatted before submitting a Pull Request. + +```bash +# Formatter +dart format . + +# Analyzer +dart analyze + +# Tests +dart test +``` + ## License MIT License. See [LICENSE](LICENSE) for details. From 2fc388eb7a58cb1e3e0fd1a22a35006aafb0f777 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 31 May 2026 23:00:13 +0300 Subject: [PATCH 4/5] doc: improvements --- lib/src/binary_reader.dart | 16 ++++++++-------- lib/src/binary_writer.dart | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 25fbf32..2412eb0 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -650,7 +650,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 1]. + /// Throws [RangeError] if [position] is negative or beyond `length - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt16(int position, [Endian endian = Endian.big]) { @@ -666,7 +666,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 1]. + /// Throws [RangeError] if [position] is negative or beyond `length - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint16(int position, [Endian endian = Endian.big]) { @@ -682,7 +682,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + /// Throws [RangeError] if [position] is negative or beyond `length - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt32(int position, [Endian endian = Endian.big]) { @@ -698,7 +698,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + /// Throws [RangeError] if [position] is negative or beyond `length - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint32(int position, [Endian endian = Endian.big]) { @@ -714,7 +714,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + /// Throws [RangeError] if [position] is negative or beyond `length - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt64(int position, [Endian endian = Endian.big]) { @@ -730,7 +730,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + /// Throws [RangeError] if [position] is negative or beyond `length - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint64(int position, [Endian endian = Endian.big]) { @@ -746,7 +746,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 3]. + /// Throws [RangeError] if [position] is negative or beyond `length - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double getFloat32(int position, [Endian endian = Endian.big]) { @@ -762,7 +762,7 @@ extension BinaryReaderRandomAccess on BinaryReader { /// /// [endian] specifies byte order (defaults to big-endian). /// - /// Throws [RangeError] if [position] is negative or beyond [length - 7]. + /// Throws [RangeError] if [position] is negative or beyond `length - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double getFloat64(int position, [Endian endian = Endian.big]) { diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index b656349..e031741 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -863,7 +863,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 1]. + /// `bytesWritten - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt16(int position, [Endian endian = Endian.big]) { @@ -880,7 +880,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 1]. + /// `bytesWritten - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint16(int position, [Endian endian = Endian.big]) { @@ -897,7 +897,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt32(int position, [Endian endian = Endian.big]) { @@ -914,7 +914,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint32(int position, [Endian endian = Endian.big]) { @@ -931,7 +931,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getInt64(int position, [Endian endian = Endian.big]) { @@ -948,7 +948,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int getUint64(int position, [Endian endian = Endian.big]) { @@ -965,7 +965,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double getFloat32(int position, [Endian endian = Endian.big]) { @@ -982,7 +982,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double getFloat64(int position, [Endian endian = Endian.big]) { @@ -1014,7 +1014,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 1]. + /// `bytesWritten - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setInt16(int position, int value, [Endian endian = Endian.big]) { @@ -1031,7 +1031,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 1]. + /// `bytesWritten - 1`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setUint16(int position, int value, [Endian endian = Endian.big]) { @@ -1048,7 +1048,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setInt32(int position, int value, [Endian endian = Endian.big]) { @@ -1065,7 +1065,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setUint32(int position, int value, [Endian endian = Endian.big]) { @@ -1082,7 +1082,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setInt64(int position, int value, [Endian endian = Endian.big]) { @@ -1099,7 +1099,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setUint64(int position, int value, [Endian endian = Endian.big]) { @@ -1116,7 +1116,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 3]. + /// `bytesWritten - 3`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setFloat32(int position, double value, [Endian endian = Endian.big]) { @@ -1132,7 +1132,7 @@ extension BinaryWriterRandomAccess on BinaryWriter { /// [endian] specifies byte order (defaults to big-endian). /// /// Throws [RangeError] if [position] is negative or beyond - /// [bytesWritten - 7]. + /// `bytesWritten - 7`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void setFloat64(int position, double value, [Endian endian = Endian.big]) { From dc2e109e6a4b2c6a17497c01ab0c8a78090216cd Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 31 May 2026 23:03:11 +0300 Subject: [PATCH 5/5] example: improvements --- example/basic/main.dart | 3 ++- example/file_streaming/main.dart | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/example/basic/main.dart b/example/basic/main.dart index bcadbef..62960ff 100644 --- a/example/basic/main.dart +++ b/example/basic/main.dart @@ -35,7 +35,8 @@ void main() { _log('\nPool API'); final pooledBytes = BinaryWriterPool.withWriter((w) { user.encode(w); - return w.toBytes(); // toBytes() returns a zero-copy view + // safely copy data, keeping internal buffer pooled + return w.takeBytes(copy: true); }); _log(' Pooled serialization done: ${pooledBytes.length} bytes'); diff --git a/example/file_streaming/main.dart b/example/file_streaming/main.dart index 9494f8f..e6bc480 100644 --- a/example/file_streaming/main.dart +++ b/example/file_streaming/main.dart @@ -35,14 +35,12 @@ void main() async { ).encode(writer); if (writer.bytesWritten >= 64000) { - ios.add(writer.toBytes()); - writer.seek(0); + ios.add(writer.takeBytes(copy: true)); } } if (writer.bytesWritten > 0) { - ios.add(writer.toBytes()); - writer.seek(0); + ios.add(writer.takeBytes(copy: true)); } writeWatch.stop();