From c42bcf2a77c42758ab98cc2ce385f025c6f813da Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 12:01:15 +0300 Subject: [PATCH 01/17] **BREAKING CHANGES:** MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **BinaryReader**: removed `reset()` method — use `seek(0)` instead **New Features:** - **BinaryReader**: added `rebind(Uint8List)` — rebinds the reader to a new buffer without allocating a new instance (useful for streaming scenarios) **Fixes:** - **BinaryReader**: added bounds check to `peekByte()` — now throws `RangeError` consistently like other read methods **Tests:** - Added tests for `BinaryReader.rebind()` — normal rebind, partial reads, zero-length buffer, identity preservation, multiple rebinds, non-zero buffer offset --- CHANGELOG.md | 18 + lib/src/binary_reader.dart | 79 +- test/integration/integration_test.dart | 17 - test/performance/deserialization_bench.dart | 6 +- test/unit/binary_reader_test.dart | 2514 ++++++++++--------- 5 files changed, 1349 insertions(+), 1285 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 364f8c0..6255dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 3.3.0 + +**BREAKING CHANGES:** + +- **BinaryReader**: removed `reset()` method — use `seek(0)` instead + +**New Features:** + +- **BinaryReader**: added `rebind(Uint8List)` — rebinds the reader to a new buffer without allocating a new instance (useful for streaming scenarios) + +**Fixes:** + +- **BinaryReader**: added bounds check to `peekByte()` — now throws `RangeError` consistently like other read methods + +**Tests:** + +- Added tests for `BinaryReader.rebind()` — normal rebind, partial reads, zero-length buffer, identity preservation, multiple rebinds, non-zero buffer offset + ## 3.2.0 **BREAKING CHANGES:** diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 0d2b4d2..9b6b230 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -10,7 +10,7 @@ import 'dart:typed_data'; /// - Byte arrays and strings /// /// The reader maintains an internal offset that advances as data is read. -/// Use [reset] to restart reading from the beginning. +/// Use [seek] with position `0` to restart reading from the beginning. /// /// Example: /// ```dart @@ -24,7 +24,7 @@ import 'dart:typed_data'; /// // Check remaining data /// print('Bytes left: ${reader.availableBytes}'); /// ``` -extension type const BinaryReader._(_ReaderState _rs) { +extension type BinaryReader._(_ReaderState _rs) { /// Creates a new [BinaryReader] from the given byte buffer. /// /// The reader will start at position 0 and can read up to `buffer.length` @@ -589,6 +589,26 @@ extension type const BinaryReader._(_ReaderState _rs) { return _rs.data.buffer.asUint8List(bOffset + peekOffset, length); } + /// 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. + /// + /// Example: + /// ```dart + /// final nextByte = reader.peekByte(); + /// if (nextByte == 0x42) { + /// // Handle type 0x42 + /// } + /// final actualByte = reader.readUint8(); // Now read it + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int peekByte() { + _checkBounds(1, 'Peek Byte'); + return _rs.list[_rs.offset]; + } + /// Advances the read position by the specified number of bytes. /// /// This is useful for skipping over data you don't need to process. @@ -660,13 +680,18 @@ extension type const BinaryReader._(_ReaderState _rs) { _rs.offset -= length; } - /// Resets the read position to the beginning of the buffer. + + /// Rebinds the reader to a new buffer without creating a new [BinaryReader]. /// - /// This allows re-reading the same data without creating a new reader. + /// 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 reset() { - _rs.offset = 0; + void rebind(Uint8List buffer) { + _rs.rebind(buffer); } /// Returns the byte at the specified absolute [index] in the buffer. @@ -681,23 +706,6 @@ extension type const BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int operator [](int index) => _rs.list[index]; - /// 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. - /// - /// Example: - /// ```dart - /// final nextByte = reader.peekByte(); - /// if (nextByte == 0x42) { - /// // Handle type 0x42 - /// } - /// final actualByte = reader.readUint8(); // Now read it - /// ``` - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - int peekByte() => _rs.list[_rs.offset]; - /// Reads [length] bytes from the current position. /// /// This is a concise alias for [readBytes]. @@ -747,21 +755,36 @@ final class _ReaderState { offset = 0; /// Direct access to the underlying byte list. - final Uint8List list; + Uint8List list; /// Efficient view for typed data access (getInt32, getFloat64, etc.). - final ByteData data; + ByteData data; /// The underlying byte buffer. - final ByteBuffer buffer; + ByteBuffer buffer; /// Total length of the buffer in bytes. - final int length; + int length; /// Current read position in the buffer. int offset; /// Offset of the buffer view within its underlying [ByteBuffer]. /// Necessary for creating accurate subviews. - final int baseOffset; + int baseOffset; + + /// Rebinds this state to a new buffer without creating a new [_ReaderState]. + /// + /// Updates all buffer-related fields and resets [offset] to 0. + /// The old buffer is discarded and becomes eligible for GC. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void rebind(Uint8List buffer) { + list = buffer; + data = ByteData.sublistView(buffer).asUnmodifiableView(); + this.buffer = buffer.buffer; + length = buffer.length; + baseOffset = buffer.offsetInBytes; + offset = 0; + } } diff --git a/test/integration/integration_test.dart b/test/integration/integration_test.dart index 7a6b8d8..ecf4fa9 100644 --- a/test/integration/integration_test.dart +++ b/test/integration/integration_test.dart @@ -550,23 +550,6 @@ void main() { expect(reader.readUint32(), equals(42)); }); - test('reset reader after partial read', () { - final writer = BinaryWriter() - ..writeUint32(100) - ..writeUint32(200) - ..writeUint32(300); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint32(), equals(100)); - expect(reader.offset, equals(4)); - - reader.reset(); - expect(reader.offset, equals(0)); - expect(reader.readUint32(), equals(100)); - }); - test('offset tracking during read', () { final writer = BinaryWriter() ..writeUint8(1) diff --git a/test/performance/deserialization_bench.dart b/test/performance/deserialization_bench.dart index 03102c6..c70fbe2 100644 --- a/test/performance/deserialization_bench.dart +++ b/test/performance/deserialization_bench.dart @@ -25,7 +25,7 @@ class SimpleMessageReadBenchmark extends BenchmarkBase { @override void run() { - reader.reset(); + reader.seek(0); _checksum += reader.readUint32(); _checksum += reader.readFloat32().toInt(); _checksum += reader.readBool() ? 1 : 0; @@ -70,7 +70,7 @@ class ComplexProfileReadBenchmark extends BenchmarkBase { @override void run() { - reader.reset(); + reader.seek(0); _checksum += reader.readVarUint(); _checksum += reader.readVarString().length; _checksum += reader.readVarString().length; @@ -114,7 +114,7 @@ class LargeArrayReadBenchmark extends BenchmarkBase { @override void run() { - reader.reset(); + reader.seek(0); final count = reader.readVarUint(); for (var i = 0; i < count; i++) { _checksum += reader.readUint32(); diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 46de277..898e6ec 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -703,1451 +703,1491 @@ void main() { expect(reader.offset, equals(4)); }); - test('offset resets to 0 after reset', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); - expect(reader.offset, equals(1)); - expect(reader.availableBytes, equals(2)); - - reader.reset(); - expect(reader.offset, equals(0)); - expect(reader.availableBytes, equals(3)); - }); - }); - - group('Special values and edge cases', () { - test('readString with empty UTF-8 string', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readString(0), equals('')); - expect(reader.availableBytes, equals(0)); - }); - - test('readString with emoji characters', () { - const str = '🚀👨‍👩‍👧‍👦'; // Rocket and family emoji - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - expect(reader.availableBytes, equals(0)); - }); - - test('readFloat32 with NaN', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .nan); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('readFloat32 with Infinity', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .infinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), equals(double.infinity)); - }); - - test('readFloat32 with negative Infinity', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .negativeInfinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), equals(double.negativeInfinity)); - }); + group('Special values and edge cases', () { + test('readString with empty UTF-8 string', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); - test('readFloat64 with NaN', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .nan); - final reader = BinaryReader(buffer); + expect(reader.readString(0), equals('')); + expect(reader.availableBytes, equals(0)); + }); - expect(reader.readFloat64().isNaN, isTrue); - }); + test('readString with emoji characters', () { + const str = '🚀👨‍👩‍👧‍👦'; // Rocket and family emoji + final encoded = utf8.encode(str); + final buffer = Uint8List.fromList(encoded); + final reader = BinaryReader(buffer); - test('readFloat64 with Infinity', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .infinity); - final reader = BinaryReader(buffer); + expect(reader.readString(encoded.length), equals(str)); + expect(reader.availableBytes, equals(0)); + }); - expect(reader.readFloat64(), equals(double.infinity)); - }); + test('readFloat32 with NaN', () { + final buffer = Uint8List(4); + ByteData.view(buffer.buffer).setFloat32(0, .nan); + final reader = BinaryReader(buffer); - test('readFloat64 with negative Infinity', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .negativeInfinity); - final reader = BinaryReader(buffer); + expect(reader.readFloat32().isNaN, isTrue); + }); - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }); + test('readFloat32 with Infinity', () { + final buffer = Uint8List(4); + ByteData.view(buffer.buffer).setFloat32(0, .infinity); + final reader = BinaryReader(buffer); - test('readFloat64 with negative zero', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, -0); - final reader = BinaryReader(buffer); + expect(reader.readFloat32(), equals(double.infinity)); + }); - final value = reader.readFloat64(); - expect(value, equals(0.0)); - expect(value.isNegative, isTrue); - }); + test('readFloat32 with negative Infinity', () { + final buffer = Uint8List(4); + ByteData.view(buffer.buffer).setFloat32(0, .negativeInfinity); + final reader = BinaryReader(buffer); - test('readUint64 with maximum value', () { - final buffer = Uint8List.fromList([ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // - ]); - final reader = BinaryReader(buffer); + expect(reader.readFloat32(), equals(double.negativeInfinity)); + }); - // Max Uint64 is 2^64 - 1 = 18446744073709551615 - // In Dart, this wraps to -1 for signed int representation - expect(reader.readUint64(), equals(0xFFFFFFFFFFFFFFFF)); - }); + test('readFloat64 with NaN', () { + final buffer = Uint8List(8); + ByteData.view(buffer.buffer).setFloat64(0, .nan); + final reader = BinaryReader(buffer); - test('peekBytes with zero length', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + expect(reader.readFloat64().isNaN, isTrue); + }); - expect(reader.peekBytes(0), equals([])); - expect(reader.offset, equals(0)); - }); + test('readFloat64 with Infinity', () { + final buffer = Uint8List(8); + ByteData.view(buffer.buffer).setFloat64(0, .infinity); + final reader = BinaryReader(buffer); - test('peekBytes with explicit zero offset', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); + expect(reader.readFloat64(), equals(double.infinity)); + }); - final peeked = reader.peekBytes(2, 0); - expect(peeked, equals([0x01, 0x02])); - expect(reader.offset, equals(1)); - }); + test('readFloat64 with negative Infinity', () { + final buffer = Uint8List(8); + ByteData.view(buffer.buffer).setFloat64(0, .negativeInfinity); + final reader = BinaryReader(buffer); - test('multiple resets in sequence', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer) - ..readUint8() - ..reset() - ..reset() - ..reset(); + expect(reader.readFloat64(), equals(double.negativeInfinity)); + }); - expect(reader.offset, equals(0)); - expect(reader.availableBytes, equals(3)); - }); + test('readFloat64 with negative zero', () { + final buffer = Uint8List(8); + ByteData.view(buffer.buffer).setFloat64(0, -0); + final reader = BinaryReader(buffer); - test('read after buffer exhaustion and reset', () { - final buffer = Uint8List.fromList([0x42, 0x43]); - final reader = BinaryReader(buffer); + final value = reader.readFloat64(); + expect(value, equals(0.0)); + expect(value.isNegative, isTrue); + }); - expect(reader.readUint8(), equals(0x42)); - expect(reader.readUint8(), equals(0x43)); - expect(reader.availableBytes, equals(0)); + test('readUint64 with maximum value', () { + final buffer = Uint8List.fromList([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + ]); + final reader = BinaryReader(buffer); - reader.reset(); - expect(reader.readUint8(), equals(0x42)); - }); - }); + // Max Uint64 is 2^64 - 1 = 18446744073709551615 + // In Dart, this wraps to -1 for signed int representation + expect(reader.readUint64(), equals(0xFFFFFFFFFFFFFFFF)); + }); - group('Malformed UTF-8', () { - test('readString with allowMalformed=true handles invalid UTF-8', () { - // Invalid UTF-8 sequence: 0xFF is not valid in UTF-8 - final buffer = Uint8List.fromList([ - 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" - 0xFF, // Invalid byte - 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" - ]); - final reader = BinaryReader(buffer); + test('peekBytes with zero length', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); + final reader = BinaryReader(buffer); - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, contains('Hello')); - expect(result, contains('World')); - }); + expect(reader.peekBytes(0), equals([])); + expect(reader.offset, equals(0)); + }); - test('readString with allowMalformed=false throws on invalid UTF-8', () { - final buffer = Uint8List.fromList([0xFF, 0xFE, 0xFD]); - final reader = BinaryReader(buffer); + test('peekBytes with explicit zero offset', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); + final reader = BinaryReader(buffer)..readUint8(); - expect( - () => reader.readString(buffer.length), - throwsA(isA()), - ); + final peeked = reader.peekBytes(2, 0); + expect(peeked, equals([0x01, 0x02])); + expect(reader.offset, equals(1)); + }); }); - test('readString handles truncated multi-byte sequence', () { - final buffer = Uint8List.fromList([0xE0, 0xA0]); - final reader = BinaryReader(buffer); + group('Malformed UTF-8', () { + test('readString with allowMalformed=true handles invalid UTF-8', () { + // Invalid UTF-8 sequence: 0xFF is not valid in UTF-8 + final buffer = Uint8List.fromList([ + 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" + 0xFF, // Invalid byte + 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" + ]); + final reader = BinaryReader(buffer); - expect( - () => reader.readString(buffer.length), - throwsA(isA()), + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, contains('Hello')); + expect(result, contains('World')); + }); + + test( + 'readString with allowMalformed=false throws on invalid UTF-8', + () { + final buffer = Uint8List.fromList([0xFF, 0xFE, 0xFD]); + final reader = BinaryReader(buffer); + + expect( + () => reader.readString(buffer.length), + throwsA(isA()), + ); + }, ); - }); - - test('readString with allowMalformed handles truncated sequence', () { - final buffer = Uint8List.fromList([ - 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" - 0xE0, 0xA0, // Incomplete 3-byte sequence - ]); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, startsWith('Hello')); - }); - }); - - group('Lone surrogate pairs', () { - test('readString handles lone high surrogate', () { - final buffer = utf8.encode('Test\uD800End'); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, isNotEmpty); - }); - - test('readString handles lone low surrogate', () { - final buffer = utf8.encode('Test\uDC00End'); - final reader = BinaryReader(buffer); - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, isNotEmpty); - }); - }); - - group('peekBytes advanced', () { - test( - 'peekBytes with offset beyond current position but within buffer', - () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - final reader = BinaryReader(buffer) - ..readUint8() - ..readUint8(); - - final peeked = reader.peekBytes(3, 5); - expect(peeked, equals([6, 7, 8])); - expect(reader.offset, equals(2)); - }, - ); - - test('peekBytes at buffer boundary', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + test('readString handles truncated multi-byte sequence', () { + final buffer = Uint8List.fromList([0xE0, 0xA0]); + final reader = BinaryReader(buffer); - final peeked = reader.peekBytes(2, 3); - expect(peeked, equals([4, 5])); - expect(reader.offset, equals(0)); - }); + expect( + () => reader.readString(buffer.length), + throwsA(isA()), + ); + }); - test('peekBytes exactly at end with zero length', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer); + test('readString with allowMalformed handles truncated sequence', () { + final buffer = Uint8List.fromList([ + 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" + 0xE0, 0xA0, // Incomplete 3-byte sequence + ]); + final reader = BinaryReader(buffer); - final peeked = reader.peekBytes(0, 3); - expect(peeked, isEmpty); - expect(reader.offset, equals(0)); + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, startsWith('Hello')); + }); }); - }); - group('Sequential operations', () { - test('multiple reset calls with intermediate reads', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + group('Lone surrogate pairs', () { + test('readString handles lone high surrogate', () { + final buffer = utf8.encode('Test\uD800End'); + final reader = BinaryReader(buffer); - expect(reader.readUint8(), equals(1)); - reader.reset(); - expect(reader.readUint8(), equals(1)); - expect(reader.readUint8(), equals(2)); - reader.reset(); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, isNotEmpty); + }); - test('alternating read and peek operations', () { - final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); + test('readString handles lone low surrogate', () { + final buffer = utf8.encode('Test\uDC00End'); + final reader = BinaryReader(buffer); - expect(reader.readUint8(), equals(10)); - expect(reader.peekBytes(2), equals([20, 30])); - expect(reader.readUint8(), equals(20)); - expect(reader.peekBytes(1, 3), equals([40])); - expect(reader.readUint8(), equals(30)); + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, isNotEmpty); + }); }); - }); - group('Large buffer operations', () { - test('readBytes with very large length', () { - const largeSize = 1000000; - final buffer = Uint8List(largeSize); - for (var i = 0; i < largeSize; i++) { - buffer[i] = i % 256; - } + group('peekBytes advanced', () { + test( + 'peekBytes with offset beyond current position but within buffer', + () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + final reader = BinaryReader(buffer) + ..readUint8() + ..readUint8(); - final reader = BinaryReader(buffer); - final result = reader.readBytes(largeSize); + final peeked = reader.peekBytes(3, 5); + expect(peeked, equals([6, 7, 8])); + expect(reader.offset, equals(2)); + }, + ); - expect(result.length, equals(largeSize)); - expect(reader.availableBytes, equals(0)); - }); + test('peekBytes at buffer boundary', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - test('skip large amount of data', () { - final buffer = Uint8List(100000); - final reader = BinaryReader(buffer)..skip(50000); - expect(reader.offset, equals(50000)); - expect(reader.availableBytes, equals(50000)); - }); - }); + final peeked = reader.peekBytes(2, 3); + expect(peeked, equals([4, 5])); + expect(reader.offset, equals(0)); + }); - group('Buffer sharing', () { - test('multiple readers can read same buffer concurrently', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader1 = BinaryReader(buffer); - final reader2 = BinaryReader(buffer); + test('peekBytes exactly at end with zero length', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); - expect(reader1.readUint8(), equals(1)); - expect(reader2.readUint8(), equals(1)); - expect(reader1.readUint8(), equals(2)); - expect(reader2.readUint16(), equals(0x0203)); + final peeked = reader.peekBytes(0, 3); + expect(peeked, isEmpty); + expect(reader.offset, equals(0)); + }); }); - test('peekBytes returns independent views', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - final peek1 = reader.peekBytes(3); - final peek2 = reader.peekBytes(3); + group('Sequential operations', () { + test('alternating read and peek operations', () { + final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); + final reader = BinaryReader(buffer); - expect(peek1, equals([1, 2, 3])); - expect(peek2, equals([1, 2, 3])); - expect(identical(peek1, peek2), isFalse); + expect(reader.readUint8(), equals(10)); + expect(reader.peekBytes(2), equals([20, 30])); + expect(reader.readUint8(), equals(20)); + expect(reader.peekBytes(1, 3), equals([40])); + expect(reader.readUint8(), equals(30)); + }); }); - }); - - group('Zero-copy verification', () { - test('readBytes returns view of original buffer', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - final bytes = reader.readBytes(3); + group('Large buffer operations', () { + test('readBytes with very large length', () { + const largeSize = 1000000; + final buffer = Uint8List(largeSize); + for (var i = 0; i < largeSize; i++) { + buffer[i] = i % 256; + } - expect(bytes, isA()); - expect(bytes.length, equals(3)); - }); - - test('peekBytes returns view of original buffer', () { - final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); + final reader = BinaryReader(buffer); + final result = reader.readBytes(largeSize); + + expect(result.length, equals(largeSize)); + expect(reader.availableBytes, equals(0)); + }); + + test('skip large amount of data', () { + final buffer = Uint8List(100000); + final reader = BinaryReader(buffer)..skip(50000); + expect(reader.offset, equals(50000)); + expect(reader.availableBytes, equals(50000)); + }); + }); + + group('Buffer sharing', () { + test('multiple readers can read same buffer concurrently', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader1 = BinaryReader(buffer); + final reader2 = BinaryReader(buffer); + + expect(reader1.readUint8(), equals(1)); + expect(reader2.readUint8(), equals(1)); + expect(reader1.readUint8(), equals(2)); + expect(reader2.readUint16(), equals(0x0203)); + }); + + test('peekBytes returns independent views', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - final peeked = reader.peekBytes(3); + final peek1 = reader.peekBytes(3); + final peek2 = reader.peekBytes(3); - expect(peeked, isA()); - expect(peeked, equals([10, 20, 30])); + expect(peek1, equals([1, 2, 3])); + expect(peek2, equals([1, 2, 3])); + expect(identical(peek1, peek2), isFalse); + }); }); - }); - group('Mixed endianness operations', () { - test('reading alternating big and little endian values', () { - final writer = BinaryWriter() - ..writeUint16(0x1234) - ..writeUint16(0x5678, .little) - ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, .little); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(.little), equals(0x5678)); - expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(.little), equals(0x11223344)); - }); + group('Zero-copy verification', () { + test('readBytes returns view of original buffer', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - test('float values with different endianness', () { - final writer = BinaryWriter() - ..writeFloat32(3.14) - ..writeFloat32(2.71, .little) - ..writeFloat64(1.414) - ..writeFloat64(1.732, .little); + final bytes = reader.readBytes(3); - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); + expect(bytes, isA()); + expect(bytes.length, equals(3)); + }); - expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); - expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); - }); - }); + test('peekBytes returns view of original buffer', () { + final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); + final reader = BinaryReader(buffer); - group('Boundary conditions at exact sizes', () { - test('buffer exactly matches read size', () { - final buffer = Uint8List.fromList([1, 2, 3, 4]); - final reader = BinaryReader(buffer); + final peeked = reader.peekBytes(3); - final result = reader.readBytes(4); - expect(result, equals([1, 2, 3, 4])); - expect(reader.availableBytes, equals(0)); + expect(peeked, isA()); + expect(peeked, equals([10, 20, 30])); + }); }); - test('reading exactly to boundary multiple times', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6]); - final reader = BinaryReader(buffer); + group('Mixed endianness operations', () { + test('reading alternating big and little endian values', () { + final writer = BinaryWriter() + ..writeUint16(0x1234) + ..writeUint16(0x5678, .little) + ..writeUint32(0x9ABCDEF0) + ..writeUint32(0x11223344, .little); - expect(reader.readUint16(), equals(0x0102)); - expect(reader.readUint16(), equals(0x0304)); - expect(reader.readUint16(), equals(0x0506)); - expect(reader.availableBytes, equals(0)); - }); - }); - - group('baseOffset handling', () { - test('readBytes works correctly with non-zero baseOffset', () { - // Create a larger buffer and take a sublist - // (which will have non-zero baseOffset) - final largeBuffer = Uint8List(100); - for (var i = 0; i < 100; i++) { - largeBuffer[i] = i; - } - - // Create a view starting at offset 50 - final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); - final reader = BinaryReader(subBuffer); - - // Read bytes and verify they match the expected values (50-59) - final bytes = reader.readBytes(5); - expect(bytes, equals([50, 51, 52, 53, 54])); - expect(reader.availableBytes, equals(5)); - }); + final buffer = writer.takeBytes(); + final reader = BinaryReader(buffer); - test('readString works correctly with non-zero baseOffset', () { - // Create a buffer with text data - const text = 'Hello, World!'; - final encoded = utf8.encode(text); + expect(reader.readUint16(), equals(0x1234)); + expect(reader.readUint16(.little), equals(0x5678)); + expect(reader.readUint32(), equals(0x9ABCDEF0)); + expect(reader.readUint32(.little), equals(0x11223344)); + }); - // Create a larger buffer and copy the text at an offset - final largeBuffer = Uint8List(100) - ..setRange(30, 30 + encoded.length, encoded); + test('float values with different endianness', () { + final writer = BinaryWriter() + ..writeFloat32(3.14) + ..writeFloat32(2.71, .little) + ..writeFloat64(1.414) + ..writeFloat64(1.732, .little); - // Create a view of just the text portion - final subBuffer = Uint8List.sublistView( - largeBuffer, - 30, - 30 + encoded.length, - ); - final reader = BinaryReader(subBuffer); + final buffer = writer.takeBytes(); + final reader = BinaryReader(buffer); - final result = reader.readString(encoded.length); - expect(result, equals(text)); - expect(reader.availableBytes, equals(0)); + expect(reader.readFloat32(), closeTo(3.14, 0.01)); + expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); + expect(reader.readFloat64(), closeTo(1.414, 0.001)); + expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); + }); }); - test('peekBytes works correctly with non-zero baseOffset', () { - final largeBuffer = Uint8List(50); - for (var i = 0; i < 50; i++) { - largeBuffer[i] = i; - } - - // Create a view starting at offset 20 - final subBuffer = Uint8List.sublistView(largeBuffer, 20, 30); - final reader = BinaryReader(subBuffer); + group('Boundary conditions at exact sizes', () { + test('buffer exactly matches read size', () { + final buffer = Uint8List.fromList([1, 2, 3, 4]); + final reader = BinaryReader(buffer); - // Peek at bytes without consuming them - final peeked = reader.peekBytes(5); - expect(peeked, equals([20, 21, 22, 23, 24])); - expect(reader.offset, equals(0)); + final result = reader.readBytes(4); + expect(result, equals([1, 2, 3, 4])); + expect(reader.availableBytes, equals(0)); + }); - // Now read and verify - final read = reader.readBytes(5); - expect(read, equals([20, 21, 22, 23, 24])); - expect(reader.offset, equals(5)); - }); + test('reading exactly to boundary multiple times', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6]); + final reader = BinaryReader(buffer); - test('readUint16/32/64 work correctly with non-zero baseOffset', () { - final largeBuffer = Uint8List(100); + expect(reader.readUint16(), equals(0x0102)); + expect(reader.readUint16(), equals(0x0304)); + expect(reader.readUint16(), equals(0x0506)); + expect(reader.availableBytes, equals(0)); + }); + }); + + group('baseOffset handling', () { + test('readBytes works correctly with non-zero baseOffset', () { + // Create a larger buffer and take a sublist + // (which will have non-zero baseOffset) + final largeBuffer = Uint8List(100); + for (var i = 0; i < 100; i++) { + largeBuffer[i] = i; + } + + // Create a view starting at offset 50 + final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); + final reader = BinaryReader(subBuffer); + + // Read bytes and verify they match the expected values (50-59) + final bytes = reader.readBytes(5); + expect(bytes, equals([50, 51, 52, 53, 54])); + expect(reader.availableBytes, equals(5)); + }); + + test('readString works correctly with non-zero baseOffset', () { + // Create a buffer with text data + const text = 'Hello, World!'; + final encoded = utf8.encode(text); + + // Create a larger buffer and copy the text at an offset + final largeBuffer = Uint8List(100) + ..setRange(30, 30 + encoded.length, encoded); + + // Create a view of just the text portion + final subBuffer = Uint8List.sublistView( + largeBuffer, + 30, + 30 + encoded.length, + ); + final reader = BinaryReader(subBuffer); + + final result = reader.readString(encoded.length); + expect(result, equals(text)); + expect(reader.availableBytes, equals(0)); + }); + + test('peekBytes works correctly with non-zero baseOffset', () { + final largeBuffer = Uint8List(50); + for (var i = 0; i < 50; i++) { + largeBuffer[i] = i; + } + + // Create a view starting at offset 20 + final subBuffer = Uint8List.sublistView(largeBuffer, 20, 30); + final reader = BinaryReader(subBuffer); + + // Peek at bytes without consuming them + final peeked = reader.peekBytes(5); + expect(peeked, equals([20, 21, 22, 23, 24])); + expect(reader.offset, equals(0)); + + // Now read and verify + final read = reader.readBytes(5); + expect(read, equals([20, 21, 22, 23, 24])); + expect(reader.offset, equals(5)); + }); + + test('readUint16/32/64 work correctly with non-zero baseOffset', () { + final largeBuffer = Uint8List(100); + + // Write some values at offset 40 + final writer = BinaryWriter() + ..writeUint16(0x1234) + ..writeUint32(0x56789ABC) + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + ..writeUint64(0x0FEDCBA987654321); + + final data = writer.takeBytes(); + largeBuffer.setRange(40, 40 + data.length, data); + + // Create a view starting at offset 40 + final subBuffer = Uint8List.sublistView( + largeBuffer, + 40, + 40 + data.length, + ); + final reader = BinaryReader(subBuffer); - // Write some values at offset 40 - final writer = BinaryWriter() - ..writeUint16(0x1234) - ..writeUint32(0x56789ABC) + expect(reader.readUint16(), equals(0x1234)); + expect(reader.readUint32(), equals(0x56789ABC)); // disabling lint for large integer literal // ignore: avoid_js_rounded_ints - ..writeUint64(0x0FEDCBA987654321); - - final data = writer.takeBytes(); - largeBuffer.setRange(40, 40 + data.length, data); - - // Create a view starting at offset 40 - final subBuffer = Uint8List.sublistView( - largeBuffer, - 40, - 40 + data.length, - ); - final reader = BinaryReader(subBuffer); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint32(), equals(0x56789ABC)); - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - expect(reader.readUint64(), equals(0x0FEDCBA987654321)); - expect(reader.availableBytes, equals(0)); - }); - - test('multiple readers from different offsets', () { - final largeBuffer = Uint8List(100); - for (var i = 0; i < 100; i++) { - largeBuffer[i] = i; - } - - // Create two readers from different offsets - final reader1 = BinaryReader( - Uint8List.sublistView(largeBuffer, 10, 20), - ); - final reader2 = BinaryReader( - Uint8List.sublistView(largeBuffer, 50, 60), - ); - - expect(reader1.readUint8(), equals(10)); - expect(reader2.readUint8(), equals(50)); - - expect(reader1.readBytes(3), equals([11, 12, 13])); - expect(reader2.readBytes(3), equals([51, 52, 53])); - }); - - test('readVarBytes basic usage', () { - final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([1, 2, 3, 4])); - }); - - test('readVarBytes with empty array', () { - final writer = BinaryWriter()..writeVarBytes([]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([])); - }); - - test('readVarBytes multiple arrays', () { - final writer = BinaryWriter() - ..writeVarBytes([10, 20]) - ..writeVarBytes([30, 40, 50]) - ..writeVarBytes([60]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([10, 20])); - expect(reader.readVarBytes(), equals([30, 40, 50])); - expect(reader.readVarBytes(), equals([60])); - }); - - test('readVarBytes with large array', () { - final writer = BinaryWriter(); - final data = List.generate(500, (i) => (i * 3) & 0xFF); - writer.writeVarBytes(data); - final reader = BinaryReader(writer.takeBytes()); - - final result = reader.readVarBytes(); - expect(result, equals(data)); - expect(result.length, equals(500)); - }); - - test('readVarBytes throws on truncated length', () { - final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint - final reader = BinaryReader(bytes); - - expect( - reader.readVarBytes, - throwsA(isA()), - ); - }); - - test('readVarBytes throws when not enough data', () { - final bytes = Uint8List.fromList([5, 1, 2]); // Length=5, only 2 bytes - final reader = BinaryReader(bytes); - - expect( - reader.readVarBytes, - throwsA(isA()), - ); - }); - - test('readVarBytes preserves binary data', () { - final writer = BinaryWriter(); - // Test with all byte values 0-255 - final allBytes = List.generate(256, (i) => i); - writer.writeVarBytes(allBytes); - - final reader = BinaryReader(writer.takeBytes()); - final result = reader.readVarBytes(); - - expect(result, equals(allBytes)); - for (var i = 0; i < 256; i++) { - expect(result[i], equals(i), reason: 'Byte $i mismatch'); - } - }); - - test('readVarString basic usage', () { - final writer = BinaryWriter()..writeVarString('Hello'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('Hello')); - }); - - test('readVarString with UTF-8 multi-byte', () { - final writer = BinaryWriter()..writeVarString('世界'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('世界')); - }); - - test('readVarString with emoji', () { - final writer = BinaryWriter()..writeVarString('🌍🎉'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('🌍🎉')); - }); - - test('readVarString with empty string', () { - final writer = BinaryWriter()..writeVarString(''); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('')); - }); - - test('readVarString multiple strings', () { - final writer = BinaryWriter() - ..writeVarString('First') - ..writeVarString('Second 测试') - ..writeVarString('Third 🎉'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('First')); - expect(reader.readVarString(), equals('Second 测试')); - expect(reader.readVarString(), equals('Third 🎉')); - }); - - test('readVarString with allowMalformed=false on valid data', () { - final writer = BinaryWriter()..writeVarString('Valid UTF-8'); - final reader = BinaryReader(writer.takeBytes()); - - expect( - reader.readVarString, - returnsNormally, - ); - }); - - test('readVarString throws on truncated length', () { - final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint - final reader = BinaryReader(bytes); - - expect( - reader.readVarString, - throwsA(isA()), - ); - }); - - test('readVarString throws when not enough data for string', () { - final bytes = Uint8List.fromList([5, 65, 66]); // Length=5, only 2 bytes - final reader = BinaryReader(bytes); - - expect( - reader.readVarString, - throwsA(isA()), - ); - }); - - test('baseOffset with readString containing multi-byte UTF-8', () { - const text = 'Привет мир! 🌍'; - final encoded = utf8.encode(text); - - final largeBuffer = Uint8List(200) - ..setRange(75, 75 + encoded.length, encoded); - - final subBuffer = Uint8List.sublistView( - largeBuffer, - 75, - 75 + encoded.length, - ); - final reader = BinaryReader(subBuffer); - - final result = reader.readString(encoded.length); - expect(result, equals(text)); - }); - }); - - group('Getter properties', () { - test('offset getter returns current read position', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint16(2) - ..writeUint32(3); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.offset, equals(0)); - reader.readUint8(); - expect(reader.offset, equals(1)); - reader.readUint16(); - expect(reader.offset, equals(3)); - reader.readUint32(); - expect(reader.offset, equals(7)); - }); + expect(reader.readUint64(), equals(0x0FEDCBA987654321)); + expect(reader.availableBytes, equals(0)); + }); + + test('multiple readers from different offsets', () { + final largeBuffer = Uint8List(100); + for (var i = 0; i < 100; i++) { + largeBuffer[i] = i; + } + + // Create two readers from different offsets + final reader1 = BinaryReader( + Uint8List.sublistView(largeBuffer, 10, 20), + ); + final reader2 = BinaryReader( + Uint8List.sublistView(largeBuffer, 50, 60), + ); - test('length getter returns total buffer length', () { - final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(bytes); + expect(reader1.readUint8(), equals(10)); + expect(reader2.readUint8(), equals(50)); - expect(reader.length, equals(5)); - reader.readUint8(); - expect(reader.length, equals(5)); // Length doesn't change - reader.readUint32(); - expect(reader.length, equals(5)); - }); + expect(reader1.readBytes(3), equals([11, 12, 13])); + expect(reader2.readBytes(3), equals([51, 52, 53])); + }); - test('offset and length used together to calculate availableBytes', () { - final bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(bytes); + test('readVarBytes basic usage', () { + final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); + final reader = BinaryReader(writer.takeBytes()); - expect(reader.length, equals(8)); - expect(reader.offset, equals(0)); - expect(reader.availableBytes, equals(8)); + expect(reader.readVarBytes(), equals([1, 2, 3, 4])); + }); - reader.readUint32(); - expect(reader.offset, equals(4)); - expect(reader.length, equals(8)); - expect(reader.availableBytes, equals(4)); + test('readVarBytes with empty array', () { + final writer = BinaryWriter()..writeVarBytes([]); + final reader = BinaryReader(writer.takeBytes()); - reader.readUint32(); - expect(reader.offset, equals(8)); - expect(reader.length, equals(8)); - expect(reader.availableBytes, equals(0)); - }); - }); + expect(reader.readVarBytes(), equals([])); + }); - group('readBool', () { - test('reads false when byte is 0', () { - final buffer = Uint8List.fromList([0x00]); - final reader = BinaryReader(buffer); + test('readVarBytes multiple arrays', () { + final writer = BinaryWriter() + ..writeVarBytes([10, 20]) + ..writeVarBytes([30, 40, 50]) + ..writeVarBytes([60]); + final reader = BinaryReader(writer.takeBytes()); - expect(reader.readBool(), isFalse); - expect(reader.availableBytes, equals(0)); - }); + expect(reader.readVarBytes(), equals([10, 20])); + expect(reader.readVarBytes(), equals([30, 40, 50])); + expect(reader.readVarBytes(), equals([60])); + }); - test('reads true when byte is 1', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); + test('readVarBytes with large array', () { + final writer = BinaryWriter(); + final data = List.generate(500, (i) => (i * 3) & 0xFF); + writer.writeVarBytes(data); + final reader = BinaryReader(writer.takeBytes()); - expect(reader.readBool(), isTrue); - expect(reader.availableBytes, equals(0)); - }); + final result = reader.readVarBytes(); + expect(result, equals(data)); + expect(result.length, equals(500)); + }); - test('reads true when byte is any non-zero value', () { - final testValues = [1, 42, 127, 128, 255]; - for (final value in testValues) { - final buffer = Uint8List.fromList([value]); - final reader = BinaryReader(buffer); + test('readVarBytes throws on truncated length', () { + final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint + final reader = BinaryReader(bytes); expect( - reader.readBool(), - isTrue, - reason: 'Value $value should be true', + reader.readVarBytes, + throwsA(isA()), ); - } - }); - - test('reads multiple boolean values correctly', () { - final buffer = Uint8List.fromList([0x01, 0x00, 0xFF, 0x00, 0x01]); - final reader = BinaryReader(buffer); + }); - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - expect(reader.availableBytes, equals(0)); - }); - - test('advances offset correctly', () { - final buffer = Uint8List.fromList([0x01, 0x00, 0xFF]); - final reader = BinaryReader(buffer); + test('readVarBytes throws when not enough data', () { + final bytes = Uint8List.fromList([5, 1, 2]); // Length=5, only 2 bytes + final reader = BinaryReader(bytes); - expect(reader.offset, equals(0)); - reader.readBool(); - expect(reader.offset, equals(1)); - reader.readBool(); - expect(reader.offset, equals(2)); - reader.readBool(); - expect(reader.offset, equals(3)); - }); - - test('throws when reading from empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readBool, throwsA(isA())); - }); - - test('throws when no bytes available', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer)..readBool(); // Consume the byte - expect(reader.readBool, throwsA(isA())); - }); - }); - - group('readRemainingBytes', () { - test('reads all remaining bytes from start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + expect( + reader.readVarBytes, + throwsA(isA()), + ); + }); - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([1, 2, 3, 4, 5])); - expect(reader.availableBytes, equals(0)); - }); + test('readVarBytes preserves binary data', () { + final writer = BinaryWriter(); + // Test with all byte values 0-255 + final allBytes = List.generate(256, (i) => i); + writer.writeVarBytes(allBytes); - test('reads remaining bytes after partial read', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - // Read first 2 bytes - ..readUint16(); + final reader = BinaryReader(writer.takeBytes()); + final result = reader.readVarBytes(); - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([3, 4, 5, 6, 7, 8])); - expect(reader.availableBytes, equals(0)); - }); + expect(result, equals(allBytes)); + for (var i = 0; i < 256; i++) { + expect(result[i], equals(i), reason: 'Byte $i mismatch'); + } + }); - test('returns empty list when at end of buffer', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer)..readBytes(3); // Read all bytes - final remaining = reader.readRemainingBytes(); - expect(remaining, isEmpty); - expect(reader.availableBytes, equals(0)); - }); + test('readVarString basic usage', () { + final writer = BinaryWriter()..writeVarString('Hello'); + final reader = BinaryReader(writer.takeBytes()); - test('returns empty list for empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + expect(reader.readVarString(), equals('Hello')); + }); - final remaining = reader.readRemainingBytes(); - expect(remaining, isEmpty); - expect(reader.availableBytes, equals(0)); - }); + test('readVarString with UTF-8 multi-byte', () { + final writer = BinaryWriter()..writeVarString('世界'); + final reader = BinaryReader(writer.takeBytes()); - test('is zero-copy operation', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - // Skip first byte - ..readUint8(); + expect(reader.readVarString(), equals('世界')); + }); - final remaining = reader.readRemainingBytes(); - // Verify it's a view by checking buffer reference - expect(remaining.buffer, equals(buffer.buffer)); - }); + test('readVarString with emoji', () { + final writer = BinaryWriter()..writeVarString('🌍🎉'); + final reader = BinaryReader(writer.takeBytes()); - test('can be called multiple times at end', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer)..readBytes(3); + expect(reader.readVarString(), equals('🌍🎉')); + }); - final first = reader.readRemainingBytes(); - final second = reader.readRemainingBytes(); + test('readVarString with empty string', () { + final writer = BinaryWriter()..writeVarString(''); + final reader = BinaryReader(writer.takeBytes()); - expect(first, isEmpty); - expect(second, isEmpty); - }); + expect(reader.readVarString(), equals('')); + }); - test('works correctly after seek', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(2); + test('readVarString multiple strings', () { + final writer = BinaryWriter() + ..writeVarString('First') + ..writeVarString('Second 测试') + ..writeVarString('Third 🎉'); + final reader = BinaryReader(writer.takeBytes()); - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([3, 4, 5])); - }); - }); + expect(reader.readVarString(), equals('First')); + expect(reader.readVarString(), equals('Second 测试')); + expect(reader.readVarString(), equals('Third 🎉')); + }); - group('hasBytes', () { - test('returns true when enough bytes available', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + test('readVarString with allowMalformed=false on valid data', () { + final writer = BinaryWriter()..writeVarString('Valid UTF-8'); + final reader = BinaryReader(writer.takeBytes()); - expect(reader.hasBytes(1), isTrue); - expect(reader.hasBytes(3), isTrue); - expect(reader.hasBytes(5), isTrue); - }); + expect( + reader.readVarString, + returnsNormally, + ); + }); - test('returns false when not enough bytes available', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + test('readVarString throws on truncated length', () { + final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint + final reader = BinaryReader(bytes); - expect(reader.hasBytes(6), isFalse); - expect(reader.hasBytes(10), isFalse); - expect(reader.hasBytes(100), isFalse); - }); + expect( + reader.readVarString, + throwsA(isA()), + ); + }); - test('returns true for exact remaining bytes', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes - expect(reader.hasBytes(3), isTrue); // Exactly 3 bytes left - expect(reader.hasBytes(4), isFalse); // Too many - }); + test('readVarString throws when not enough data for string', () { + final bytes = Uint8List.fromList([ + 5, + 65, + 66, + ]); // Length=5, only 2 bytes + final reader = BinaryReader(bytes); - test('returns true for zero bytes on non-empty buffer', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer); + expect( + reader.readVarString, + throwsA(isA()), + ); + }); - expect(reader.hasBytes(0), isTrue); - }); + test('baseOffset with readString containing multi-byte UTF-8', () { + const text = 'Привет мир! 🌍'; + final encoded = utf8.encode(text); - test('returns true for zero bytes on empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final largeBuffer = Uint8List(200) + ..setRange(75, 75 + encoded.length, encoded); - expect(reader.hasBytes(0), isTrue); - expect(reader.hasBytes(1), isFalse); - }); + final subBuffer = Uint8List.sublistView( + largeBuffer, + 75, + 75 + encoded.length, + ); + final reader = BinaryReader(subBuffer); + + final result = reader.readString(encoded.length); + expect(result, equals(text)); + }); + }); + + group('Getter properties', () { + test('offset getter returns current read position', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeUint16(2) + ..writeUint32(3); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.offset, equals(0)); + reader.readUint8(); + expect(reader.offset, equals(1)); + reader.readUint16(); + expect(reader.offset, equals(3)); + reader.readUint32(); + expect(reader.offset, equals(7)); + }); + + test('length getter returns total buffer length', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(bytes); + + expect(reader.length, equals(5)); + reader.readUint8(); + expect(reader.length, equals(5)); // Length doesn't change + reader.readUint32(); + expect(reader.length, equals(5)); + }); + + test('offset and length used together to calculate availableBytes', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(bytes); + + expect(reader.length, equals(8)); + expect(reader.offset, equals(0)); + expect(reader.availableBytes, equals(8)); + + reader.readUint32(); + expect(reader.offset, equals(4)); + expect(reader.length, equals(8)); + expect(reader.availableBytes, equals(4)); + + reader.readUint32(); + expect(reader.offset, equals(8)); + expect(reader.length, equals(8)); + expect(reader.availableBytes, equals(0)); + }); + }); + + group('readBool', () { + test('reads false when byte is 0', () { + final buffer = Uint8List.fromList([0x00]); + final reader = BinaryReader(buffer); - test('works correctly after reading', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer); + expect(reader.readBool(), isFalse); + expect(reader.availableBytes, equals(0)); + }); - expect(reader.hasBytes(8), isTrue); - reader.readUint32(); // Read 4 bytes - expect(reader.hasBytes(5), isFalse); - expect(reader.hasBytes(4), isTrue); - reader.readUint32(); // Read 4 more bytes - expect(reader.hasBytes(1), isFalse); - expect(reader.hasBytes(0), isTrue); - }); + test('reads true when byte is 1', () { + final buffer = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer); - test('does not modify offset', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + expect(reader.readBool(), isTrue); + expect(reader.availableBytes, equals(0)); + }); + + test('reads true when byte is any non-zero value', () { + final testValues = [1, 42, 127, 128, 255]; + for (final value in testValues) { + final buffer = Uint8List.fromList([value]); + final reader = BinaryReader(buffer); + + expect( + reader.readBool(), + isTrue, + reason: 'Value $value should be true', + ); + } + }); + + test('reads multiple boolean values correctly', () { + final buffer = Uint8List.fromList([0x01, 0x00, 0xFF, 0x00, 0x01]); + final reader = BinaryReader(buffer); - expect(reader.offset, equals(0)); - reader.hasBytes(3); - expect(reader.offset, equals(0)); // Offset unchanged - reader.hasBytes(10); - expect(reader.offset, equals(0)); // Still unchanged - }); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.availableBytes, equals(0)); + }); - test('works correctly after seek', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(3); + test('advances offset correctly', () { + final buffer = Uint8List.fromList([0x01, 0x00, 0xFF]); + final reader = BinaryReader(buffer); - expect(reader.hasBytes(2), isTrue); - expect(reader.hasBytes(3), isFalse); - expect(reader.offset, equals(3)); // Unchanged - }); + expect(reader.offset, equals(0)); + reader.readBool(); + expect(reader.offset, equals(1)); + reader.readBool(); + expect(reader.offset, equals(2)); + reader.readBool(); + expect(reader.offset, equals(3)); + }); - test('works correctly after rewind', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(4) - ..rewind(2); + test('throws when reading from empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); - expect(reader.hasBytes(3), isTrue); - expect(reader.hasBytes(4), isFalse); - }); - }); + expect(reader.readBool, throwsA(isA())); + }); - group('seek', () { - test('sets position to beginning', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readUint32() // Move to position 4 - ..seek(0); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); + test('throws when no bytes available', () { + final buffer = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer)..readBool(); // Consume the byte + expect(reader.readBool, throwsA(isA())); + }); }); - test('sets position to middle', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(2); - expect(reader.offset, equals(2)); - expect(reader.readUint8(), equals(3)); - }); + group('readRemainingBytes', () { + test('reads all remaining bytes from start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - test('sets position to end', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(5); - expect(reader.offset, equals(5)); - expect(reader.availableBytes, equals(0)); - }); + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([1, 2, 3, 4, 5])); + expect(reader.availableBytes, equals(0)); + }); - test('allows seeking backwards', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(4) // Move to position 4 - ..seek(1); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); - }); + test('reads remaining bytes after partial read', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer) + // Read first 2 bytes + ..readUint16(); + + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([3, 4, 5, 6, 7, 8])); + expect(reader.availableBytes, equals(0)); + }); + + test('returns empty list when at end of buffer', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer)..readBytes(3); // Read all bytes + final remaining = reader.readRemainingBytes(); + expect(remaining, isEmpty); + expect(reader.availableBytes, equals(0)); + }); + + test('returns empty list for empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); - test('allows seeking forwards', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - ..readUint8() // Move to position 1 - ..seek(5); - expect(reader.offset, equals(5)); - expect(reader.readUint8(), equals(6)); - }); + final remaining = reader.readRemainingBytes(); + expect(remaining, isEmpty); + expect(reader.availableBytes, equals(0)); + }); - test('seeking multiple times', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..seek(3); - expect(reader.offset, equals(3)); - reader.seek(1); - expect(reader.offset, equals(1)); - reader.seek(7); - expect(reader.offset, equals(7)); - reader.seek(0); - expect(reader.offset, equals(0)); - }); + test('is zero-copy operation', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + // Skip first byte + ..readUint8(); - test('seeking to same position is valid', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..seek(2) - ..seek(2); - expect(reader.offset, equals(2)); - }); + final remaining = reader.readRemainingBytes(); + // Verify it's a view by checking buffer reference + expect(remaining.buffer, equals(buffer.buffer)); + }); - test('throws on negative position', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + test('can be called multiple times at end', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer)..readBytes(3); - expect(() => reader.seek(-1), throwsA(isA())); - }); + final first = reader.readRemainingBytes(); + final second = reader.readRemainingBytes(); - test('throws when seeking beyond buffer', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + expect(first, isEmpty); + expect(second, isEmpty); + }); - expect(() => reader.seek(6), throwsA(isA())); - expect(() => reader.seek(100), throwsA(isA())); - }); - }); + test('works correctly after seek', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(2); - group('rewind', () { - test('moves back by specified bytes', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(3) // Move to position 3 - ..rewind(2); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([3, 4, 5])); + }); }); - test('rewind to beginning', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(3) - ..rewind(3); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); + group('hasBytes', () { + test('returns true when enough bytes available', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - test('rewind single byte', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes - expect(reader.offset, equals(2)); - reader.rewind(1); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); - }); + expect(reader.hasBytes(1), isTrue); + expect(reader.hasBytes(3), isTrue); + expect(reader.hasBytes(5), isTrue); + }); - test('rewind zero bytes does nothing', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); - final offsetBefore = reader.offset; - reader.rewind(0); - expect(reader.offset, equals(offsetBefore)); - }); + test('returns false when not enough bytes available', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - test('allows re-reading data', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); + expect(reader.hasBytes(6), isFalse); + expect(reader.hasBytes(10), isFalse); + expect(reader.hasBytes(100), isFalse); + }); - final first = reader.readUint32(); - expect(first, equals(0x01020304)); + test('returns true for exact remaining bytes', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes + expect(reader.hasBytes(3), isTrue); // Exactly 3 bytes left + expect(reader.hasBytes(4), isFalse); // Too many + }); - reader.rewind(4); - final second = reader.readUint32(); - expect(second, equals(0x01020304)); - expect(second, equals(first)); - }); + test('returns true for zero bytes on non-empty buffer', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); - test('multiple rewinds', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..readBytes(5); // Position 5 - expect(reader.offset, equals(5)); + expect(reader.hasBytes(0), isTrue); + }); - reader.rewind(2); // Position 3 - expect(reader.offset, equals(3)); + test('returns true for zero bytes on empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); - reader.rewind(1); // Position 2 - expect(reader.offset, equals(2)); + expect(reader.hasBytes(0), isTrue); + expect(reader.hasBytes(1), isFalse); + }); - expect(reader.readUint8(), equals(3)); - }); + test('works correctly after reading', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer); - test('rewind and seek together', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - ..seek(5) - ..rewind(2); - expect(reader.offset, equals(3)); + expect(reader.hasBytes(8), isTrue); + reader.readUint32(); // Read 4 bytes + expect(reader.hasBytes(5), isFalse); + expect(reader.hasBytes(4), isTrue); + reader.readUint32(); // Read 4 more bytes + expect(reader.hasBytes(1), isFalse); + expect(reader.hasBytes(0), isTrue); + }); + + test('does not modify offset', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - reader.rewind(3); - expect(reader.offset, equals(0)); - }); + expect(reader.offset, equals(0)); + reader.hasBytes(3); + expect(reader.offset, equals(0)); // Offset unchanged + reader.hasBytes(10); + expect(reader.offset, equals(0)); // Still unchanged + }); - test('throws when rewinding beyond start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // offset = 2 + test('works correctly after seek', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(3); - expect(() => reader.rewind(3), throwsA(isA())); - }); + expect(reader.hasBytes(2), isTrue); + expect(reader.hasBytes(3), isFalse); + expect(reader.offset, equals(3)); // Unchanged + }); - test('throws when rewinding from start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + test('works correctly after rewind', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(4) + ..rewind(2); - expect(() => reader.rewind(1), throwsA(isA())); + expect(reader.hasBytes(3), isTrue); + expect(reader.hasBytes(4), isFalse); + }); }); - test('throws on negative length', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readBytes(3); - - expect(() => reader.rewind(-1), throwsA(isA())); - }); - }); + group('seek', () { + test('sets position to beginning', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readUint32() // Move to position 4 + ..seek(0); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('sets position to middle', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(2); + expect(reader.offset, equals(2)); + expect(reader.readUint8(), equals(3)); + }); + + test('sets position to end', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(5); + expect(reader.offset, equals(5)); + expect(reader.availableBytes, equals(0)); + }); + + test('allows seeking backwards', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(4) // Move to position 4 + ..seek(1); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('allows seeking forwards', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer) + ..readUint8() // Move to position 1 + ..seek(5); + expect(reader.offset, equals(5)); + expect(reader.readUint8(), equals(6)); + }); + + test('seeking multiple times', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..seek(3); + expect(reader.offset, equals(3)); + reader.seek(1); + expect(reader.offset, equals(1)); + reader.seek(7); + expect(reader.offset, equals(7)); + reader.seek(0); + expect(reader.offset, equals(0)); + }); + + test('seeking to same position is valid', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..seek(2) + ..seek(2); + expect(reader.offset, equals(2)); + }); - group('VarInt/VarUint edge cases', () { - test('readVarUint with maximum safe 64-bit value boundary', () { - // Test value close to overflow boundary - final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); + test('throws on negative position', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); - }); + expect(() => reader.seek(-1), throwsA(isA())); + }); - test('readVarInt with maximum positive ZigZag value', () { - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); + test('throws when seeking beyond buffer', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - final reader = BinaryReader(bytes); - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); + expect(() => reader.seek(6), throwsA(isA())); + expect(() => reader.seek(100), throwsA(isA())); + }); }); - test('readVarInt with minimum negative ZigZag value', () { - final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); - final bytes = writer.takeBytes(); + group('rewind', () { + test('moves back by specified bytes', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(3) // Move to position 3 + ..rewind(2); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('rewind to beginning', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(3) + ..rewind(3); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('rewind single byte', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes + expect(reader.offset, equals(2)); + reader.rewind(1); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('rewind zero bytes does nothing', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); + final offsetBefore = reader.offset; + reader.rewind(0); + expect(reader.offset, equals(offsetBefore)); + }); + + test('allows re-reading data', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader(buffer); - final reader = BinaryReader(bytes); - expect(reader.readVarInt(), equals(-0x4000000000000000)); - }); + final first = reader.readUint32(); + expect(first, equals(0x01020304)); - test('readVarUint boundary values sequence', () { - final writer = BinaryWriter() - ..writeVarUint(0x7F) // 1 byte max - ..writeVarUint(0x80) // 2 byte min - ..writeVarUint(0x3FFF) // 2 byte max - ..writeVarUint(0x4000) // 3 byte min - ..writeVarUint(0x1FFFFF) // 3 byte max - ..writeVarUint(0x200000); // 4 byte min - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(0x7F)); - expect(reader.readVarUint(), equals(0x80)); - expect(reader.readVarUint(), equals(0x3FFF)); - expect(reader.readVarUint(), equals(0x4000)); - expect(reader.readVarUint(), equals(0x1FFFFF)); - expect(reader.readVarUint(), equals(0x200000)); - }); + reader.rewind(4); + final second = reader.readUint32(); + expect(second, equals(0x01020304)); + expect(second, equals(first)); + }); - test('readVarInt throws on value exceeding int64 range', () { - // Create buffer with VarInt that would decode to value > max int64 - // This tests overflow protection - final buffer = Uint8List.fromList([ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // - 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, // Maximum valid VarInt encoding - ]); - final reader = BinaryReader(buffer); + test('multiple rewinds', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..readBytes(5); // Position 5 + expect(reader.offset, equals(5)); - // Should successfully read maximum value without throwing - expect( - reader.readVarInt, - returnsNormally, - ); - }); - }); + reader.rewind(2); // Position 3 + expect(reader.offset, equals(3)); - group('VarBytes/VarString error handling', () { - test('readVarBytes throws when length exceeds available bytes', () { - // Write VarInt claiming 1000 bytes but only provide 10 - final writer = BinaryWriter() - ..writeVarUint(1000) - ..writeBytes(List.filled(10, 42)); + reader.rewind(1); // Position 2 + expect(reader.offset, equals(2)); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); + expect(reader.readUint8(), equals(3)); + }); - expect(reader.readVarBytes, throwsA(isA())); - }); + test('rewind and seek together', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer) + ..seek(5) + ..rewind(2); + expect(reader.offset, equals(3)); - test('readVarString throws when length exceeds available bytes', () { - // Write VarInt claiming 100 bytes but only provide 5 - final writer = BinaryWriter() - ..writeVarUint(100) - ..writeBytes([72, 101, 108, 108, 111]); // "Hello" + reader.rewind(3); + expect(reader.offset, equals(0)); + }); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); + test('throws when rewinding beyond start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // offset = 2 - expect(reader.readVarString, throwsA(isA())); - }); + expect(() => reader.rewind(3), throwsA(isA())); + }); - test('readVarBytes with corrupted length at buffer end', () { - // VarInt that claims more bytes than buffer has - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = BinaryReader(buffer); + test('throws when rewinding from start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); - // Should throw when trying to read the claimed bytes - expect(reader.readVarBytes, throwsA(isA())); - }); + expect(() => reader.rewind(1), throwsA(isA())); + }); - test('readVarString handles empty string correctly', () { - final writer = BinaryWriter()..writeVarString(''); - final bytes = writer.takeBytes(); + test('throws on negative length', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readBytes(3); - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('')); + expect(() => reader.rewind(-1), throwsA(isA())); + }); }); - test('readVarBytes with zero length', () { - final writer = BinaryWriter()..writeVarBytes([]); - final bytes = writer.takeBytes(); + group('VarInt/VarUint edge cases', () { + test('readVarUint with maximum safe 64-bit value boundary', () { + // Test value close to overflow boundary + final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - expect(reader.readVarBytes(), isEmpty); - }); - - test('readVarString with malformed UTF-8 in VarString format', () { - // Write invalid UTF-8 sequence with VarInt length prefix - final writer = BinaryWriter() - ..writeVarUint(3) - ..writeBytes([0xFF, 0xFE, 0xFD]); // Invalid UTF-8 + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); + }); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); + test('readVarInt with maximum positive ZigZag value', () { + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); - expect( - reader.readVarString, - throwsA(isA()), - ); + final reader = BinaryReader(bytes); + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); + }); + + test('readVarInt with minimum negative ZigZag value', () { + final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarInt(), equals(-0x4000000000000000)); + }); + + test('readVarUint boundary values sequence', () { + final writer = BinaryWriter() + ..writeVarUint(0x7F) // 1 byte max + ..writeVarUint(0x80) // 2 byte min + ..writeVarUint(0x3FFF) // 2 byte max + ..writeVarUint(0x4000) // 3 byte min + ..writeVarUint(0x1FFFFF) // 3 byte max + ..writeVarUint(0x200000); // 4 byte min + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(0x7F)); + expect(reader.readVarUint(), equals(0x80)); + expect(reader.readVarUint(), equals(0x3FFF)); + expect(reader.readVarUint(), equals(0x4000)); + expect(reader.readVarUint(), equals(0x1FFFFF)); + expect(reader.readVarUint(), equals(0x200000)); + }); + + test('readVarInt throws on value exceeding int64 range', () { + // Create buffer with VarInt that would decode to value > max int64 + // This tests overflow protection + final buffer = Uint8List.fromList([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, // Maximum valid VarInt encoding + ]); + final reader = BinaryReader(buffer); - // Reset and try with allowMalformed - final reader2 = BinaryReader(bytes); - final result = reader2.readVarString(allowMalformed: true); - expect(result, isNotEmpty); // Should contain replacement characters + // Should successfully read maximum value without throwing + expect( + reader.readVarInt, + returnsNormally, + ); + }); }); - }); - group('Partial read scenarios', () { - test('reading after partial VarInt consumption', () { - final writer = BinaryWriter() - ..writeVarUint(300) - ..writeUint32(0x12345678); + group('VarBytes/VarString error handling', () { + test('readVarBytes throws when length exceeds available bytes', () { + // Write VarInt claiming 1000 bytes but only provide 10 + final writer = BinaryWriter() + ..writeVarUint(1000) + ..writeBytes(List.filled(10, 42)); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(300)); - expect(reader.readUint32(), equals(0x12345678)); - expect(reader.availableBytes, equals(0)); - }); + expect(reader.readVarBytes, throwsA(isA())); + }); - test('interleaved VarInt and fixed-size reads', () { - final writer = BinaryWriter() - ..writeVarUint(127) - ..writeUint8(42) - ..writeVarInt(-1) - ..writeUint16(1000) - ..writeVarUint(128) - ..writeUint32(0xDEADBEEF); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(127)); - expect(reader.readUint8(), equals(42)); - expect(reader.readVarInt(), equals(-1)); - expect(reader.readUint16(), equals(1000)); - expect(reader.readVarUint(), equals(128)); - expect(reader.readUint32(), equals(0xDEADBEEF)); - }); + test('readVarString throws when length exceeds available bytes', () { + // Write VarInt claiming 100 bytes but only provide 5 + final writer = BinaryWriter() + ..writeVarUint(100) + ..writeBytes([72, 101, 108, 108, 111]); // "Hello" - test('readRemainingBytes after VarBytes', () { - final writer = BinaryWriter() - ..writeVarBytes([1, 2, 3]) - ..writeBytes([4, 5, 6, 7, 8]); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); + expect(reader.readVarString, throwsA(isA())); + }); - final varBytes = reader.readVarBytes(); - expect(varBytes, equals([1, 2, 3])); + test('readVarBytes with corrupted length at buffer end', () { + // VarInt that claims more bytes than buffer has + final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + final reader = BinaryReader(buffer); - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([4, 5, 6, 7, 8])); - }); - }); + // Should throw when trying to read the claimed bytes + expect(reader.readVarBytes, throwsA(isA())); + }); - group('Navigation edge cases', () { - test('seek and hasBytes combined', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..seek(3); - expect(reader.hasBytes(5), isTrue); - expect(reader.hasBytes(6), isFalse); + test('readVarString handles empty string correctly', () { + final writer = BinaryWriter()..writeVarString(''); + final bytes = writer.takeBytes(); - reader.seek(7); - expect(reader.hasBytes(1), isTrue); - expect(reader.hasBytes(2), isFalse); - }); + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('')); + }); - test('rewind to exactly zero offset', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readBytes(3); - expect(reader.offset, equals(3)); + test('readVarBytes with zero length', () { + final writer = BinaryWriter()..writeVarBytes([]); + final bytes = writer.takeBytes(); - reader.rewind(3); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), isEmpty); + }); - test('multiple seeks without reading', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer); + test('readVarString with malformed UTF-8 in VarString format', () { + // Write invalid UTF-8 sequence with VarInt length prefix + final writer = BinaryWriter() + ..writeVarUint(3) + ..writeBytes([0xFF, 0xFE, 0xFD]); // Invalid UTF-8 - for (var i = 0; i < 8; i++) { - reader.seek(i); - expect(reader.offset, equals(i)); - } - }); - }); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); - group('Concise API', () { - test('operator [] returns byte at absolute index', () { - final buffer = Uint8List.fromList([10, 20, 30, 40]); - final reader = BinaryReader(buffer); - expect(reader[0], equals(10)); - expect(reader[1], equals(20)); - expect(reader[2], equals(30)); - expect(reader[3], equals(40)); - expect(reader.offset, equals(0)); - }); + expect( + reader.readVarString, + throwsA(isA()), + ); - test('call() is an alias for readBytes', () { - final buffer = Uint8List.fromList([10, 20, 30, 40]); - final reader = BinaryReader(buffer); - final data = reader.call(2); - expect(data, equals([10, 20])); - expect(reader.offset, equals(2)); - }); - }); + // Reset and try with allowMalformed + final reader2 = BinaryReader(bytes); + final result = reader2.readVarString(allowMalformed: true); + expect(result, isNotEmpty); // Should contain replacement characters + }); + }); + + group('Partial read scenarios', () { + test('reading after partial VarInt consumption', () { + final writer = BinaryWriter() + ..writeVarUint(300) + ..writeUint32(0x12345678); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(300)); + expect(reader.readUint32(), equals(0x12345678)); + expect(reader.availableBytes, equals(0)); + }); + + test('interleaved VarInt and fixed-size reads', () { + final writer = BinaryWriter() + ..writeVarUint(127) + ..writeUint8(42) + ..writeVarInt(-1) + ..writeUint16(1000) + ..writeVarUint(128) + ..writeUint32(0xDEADBEEF); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(127)); + expect(reader.readUint8(), equals(42)); + expect(reader.readVarInt(), equals(-1)); + expect(reader.readUint16(), equals(1000)); + expect(reader.readVarUint(), equals(128)); + expect(reader.readUint32(), equals(0xDEADBEEF)); + }); + + test('readRemainingBytes after VarBytes', () { + final writer = BinaryWriter() + ..writeVarBytes([1, 2, 3]) + ..writeBytes([4, 5, 6, 7, 8]); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + final varBytes = reader.readVarBytes(); + expect(varBytes, equals([1, 2, 3])); + + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([4, 5, 6, 7, 8])); + }); + }); + + group('Navigation edge cases', () { + test('seek and hasBytes combined', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..seek(3); + expect(reader.hasBytes(5), isTrue); + expect(reader.hasBytes(6), isFalse); + + reader.seek(7); + expect(reader.hasBytes(1), isTrue); + expect(reader.hasBytes(2), isFalse); + }); + + test('rewind to exactly zero offset', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readBytes(3); + expect(reader.offset, equals(3)); + + reader.rewind(3); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('multiple seeks without reading', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer); - group('Coverage edge cases', () { - test('readVarUint throws on empty buffer', () { - final reader = BinaryReader(Uint8List(0)); - expect(reader.readVarUint, throwsA(isA())); + for (var i = 0; i < 8; i++) { + reader.seek(i); + expect(reader.offset, equals(i)); + } + }); }); - test('readVarUint throws on truncated 3-byte value', () { - final reader = BinaryReader(Uint8List.fromList([0x80, 0x80])); - expect(reader.readVarUint, throwsA(isA())); + group('Concise API', () { + test('operator [] returns byte at absolute index', () { + final buffer = Uint8List.fromList([10, 20, 30, 40]); + final reader = BinaryReader(buffer); + expect(reader[0], equals(10)); + expect(reader[1], equals(20)); + expect(reader[2], equals(30)); + expect(reader[3], equals(40)); + expect(reader.offset, equals(0)); + }); + + test('call() is an alias for readBytes', () { + final buffer = Uint8List.fromList([10, 20, 30, 40]); + final reader = BinaryReader(buffer); + final data = reader.call(2); + expect(data, equals([10, 20])); + expect(reader.offset, equals(2)); + }); }); - test('readVarUint throws on truncated multi-byte value in loop', () { - final reader = BinaryReader( - Uint8List.fromList([0x80, 0x80, 0x80, 0x80]), - ); - expect(reader.readVarUint, throwsA(isA())); - }); + group('Coverage edge cases', () { + test('readVarUint throws on empty buffer', () { + final reader = BinaryReader(Uint8List(0)); + expect(reader.readVarUint, throwsA(isA())); + }); - test('hasBytes throws on negative length', () { - final reader = BinaryReader(Uint8List(10)); - expect(() => reader.hasBytes(-1), throwsA(isA())); - }); + test('readVarUint throws on truncated 3-byte value', () { + final reader = BinaryReader(Uint8List.fromList([0x80, 0x80])); + expect(reader.readVarUint, throwsA(isA())); + }); - test('readBytes throws on negative length', () { - final reader = BinaryReader(Uint8List(10)); - expect(() => reader.readBytes(-1), throwsA(isA())); - }); + test('readVarUint throws on truncated multi-byte value in loop', () { + final reader = BinaryReader( + Uint8List.fromList([0x80, 0x80, 0x80, 0x80]), + ); + expect(reader.readVarUint, throwsA(isA())); + }); - test( - 'seek throws on out of range offset in checkBounds via peekBytes', - () { + test('hasBytes throws on negative length', () { final reader = BinaryReader(Uint8List(10)); - expect(() => reader.peekBytes(1, 11), throwsA(isA())); - }, - ); - - test('peekByte returns byte at current position without advancing', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.offset, equals(0)); - }); - - test('peekByte after read returns next byte', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])) - ..readUint8(); - expect(reader.peekByte(), equals(0x43)); - expect(reader.offset, equals(1)); - }); + expect(() => reader.hasBytes(-1), throwsA(isA())); + }); - test('peekByte at end returns last byte', () { - final reader = BinaryReader(Uint8List.fromList([0x42])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.offset, equals(0)); - }); - - test('peekByte multiple times returns same value', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.peekByte(), equals(0x42)); - expect(reader.peekByte(), equals(0x42)); - }); + test('readBytes throws on negative length', () { + final reader = BinaryReader(Uint8List(10)); + expect(() => reader.readBytes(-1), throwsA(isA())); + }); + + test( + 'seek throws on out of range offset in checkBounds via peekBytes', + () { + final reader = BinaryReader(Uint8List(10)); + expect(() => reader.peekBytes(1, 11), throwsA(isA())); + }, + ); - test('fromList creates reader from List', () { - final bytes = [0x01, 0x02, 0x03, 0x04]; - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint8(), equals(1)); - expect(reader.readUint8(), equals(2)); - expect(reader.readUint8(), equals(3)); - expect(reader.readUint8(), equals(4)); - }); + test('peekByte returns byte at current position without advancing', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.offset, equals(0)); + }); - test('fromList copies data, original list can be modified', () { - final bytes = [0x01, 0x02, 0x03, 0x04]; - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint8(), equals(1)); - bytes[0] = 0xFF; - reader.reset(); - expect(reader.readUint8(), equals(1)); - }); + test('peekByte after read returns next byte', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])) + ..readUint8(); + expect(reader.peekByte(), equals(0x43)); + expect(reader.offset, equals(1)); + }); + + test('peekByte at end returns last byte', () { + final reader = BinaryReader(Uint8List.fromList([0x42])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.offset, equals(0)); + }); + + test('peekByte multiple times returns same value', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.peekByte(), equals(0x42)); + expect(reader.peekByte(), equals(0x42)); + }); + + test('fromList creates reader from List', () { + final bytes = [0x01, 0x02, 0x03, 0x04]; + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + expect(reader.readUint8(), equals(3)); + expect(reader.readUint8(), equals(4)); + }); + + test('fromList copies data, original list can be modified', () { + final bytes = [0x01, 0x02, 0x03, 0x04]; + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint8(), equals(1)); + bytes[0] = 0xFF; + reader.seek(0); + expect(reader.readUint8(), equals(1)); + }); + + test('fromList works with Uint8List', () { + final bytes = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint32(), equals(0x01020304)); + }); + + test('fromList with empty list', () { + final reader = BinaryReader.fromList([]); + expect(reader.length, equals(0)); + expect(reader.availableBytes, equals(0)); + expect(reader.readUint8, throwsA(isA())); + }); + }); + + group('rebind', () { + test('rebind replaces buffer and resets offset to 0', () { + final buffer1 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader(buffer1)..readUint8(); + expect(reader.offset, equals(1)); + + final buffer2 = Uint8List.fromList([0x10, 0x20, 0x30]); + reader.rebind(buffer2); + + expect(reader.offset, equals(0)); + expect(reader.length, equals(3)); + expect(reader.availableBytes, equals(3)); + expect(reader.readUint8(), equals(0x10)); + expect(reader.readUint8(), equals(0x20)); + }); + + test('rebind after partial reads', () { + final buffer1 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04, 0x05]); + final reader = BinaryReader(buffer1)..readUint32(); + expect(reader.offset, equals(4)); + + final buffer2 = Uint8List.fromList([0xAA, 0xBB]); + reader.rebind(buffer2); + + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(0xAA)); + expect(reader.readUint8(), equals(0xBB)); + }); + + test('rebind with zero-length buffer', () { + final buffer1 = Uint8List.fromList([0x01, 0x02]); + final reader = BinaryReader(buffer1)..readUint8(); + + final emptyBuffer = Uint8List(0); + reader.rebind(emptyBuffer); + + expect(reader.offset, equals(0)); + expect(reader.length, equals(0)); + expect(reader.availableBytes, equals(0)); + expect(reader.readUint8, throwsA(isA())); + }); + + test('rebind preserves reader identity', () { + final buffer1 = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer1); + final readerRef = reader; + + final buffer2 = Uint8List.fromList([0x02, 0x03]); + reader.rebind(buffer2); + + expect(readerRef.length, equals(2)); + expect(readerRef.readUint8(), equals(0x02)); + }); + + test('rebind multiple times', () { + final reader = BinaryReader(Uint8List.fromList([0x01])); + + for (var i = 0; i < 5; i++) { + final buffer = Uint8List.fromList([i, i + 1, i + 2]); + reader.rebind(buffer); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(i)); + } + }); + + test('rebind with data that has non-zero buffer offset', () { + final original = Uint8List.fromList( + List.generate(10, (_) => 0), + ); + final sliced = original.buffer.asUint8List(5, 5); + sliced[0] = 0x42; + sliced[1] = 0x43; - test('fromList works with Uint8List', () { - final bytes = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint32(), equals(0x01020304)); - }); + final reader = BinaryReader(Uint8List.fromList([0x00])) + ..rebind(sliced); - test('fromList with empty list', () { - final reader = BinaryReader.fromList([]); - expect(reader.length, equals(0)); - expect(reader.availableBytes, equals(0)); - expect(reader.readUint8, throwsA(isA())); + expect(reader.readUint8(), equals(0x42)); + expect(reader.readUint8(), equals(0x43)); + }); }); }); }); From ab15a88ca4e0badcca652d018972d5fc5e66bb49 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 15:07:04 +0300 Subject: [PATCH 02/17] =?UTF-8?q?-=20**BinaryWriter**:=20added=20`seek(int?= =?UTF-8?q?=20position)`=20=E2=80=94=20sets=20the=20write=20position=20to?= =?UTF-8?q?=20the=20specified=20byte=20offset=20(useful=20for=20backtracki?= =?UTF-8?q?ng=20and=20overwriting=20data=20mid-stream)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + lib/src/binary_writer.dart | 54 +++++++- test/unit/binary_writer_seek_test.dart | 176 +++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 test/unit/binary_writer_seek_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6255dcf..0be425a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ **New Features:** - **BinaryReader**: added `rebind(Uint8List)` — rebinds the reader to a new buffer without allocating a new instance (useful for streaming scenarios) +- **BinaryWriter**: added `seek(int position)` — sets the write position to the specified byte offset (useful for backtracking and overwriting data mid-stream) +- **BinaryWriter**: added `writeUint8At(int position, int value)` — writes a byte at the specified position without changing the current write position +- **BinaryWriter**: `writeVarString` now uses `seek` internally for VarInt length rewriting **Fixes:** @@ -15,6 +18,9 @@ **Tests:** - Added tests for `BinaryReader.rebind()` — normal rebind, partial reads, zero-length buffer, identity preservation, multiple rebinds, non-zero buffer offset +- Added tests for `BinaryWriter.seek()` — seek to position 0, middle, end, negative position, beyond bytesWritten, overwrite, preserve bytesWritten +- Added tests for `BinaryWriter.writeUint8At()` — overwrite at position 0/middle/end, no change to bytesWritten/write position, negative position, beyond bytesWritten, value exceeding 255, negative value, empty writer +- Added integration tests for `writeVarString` with `seek` — ASCII, non-ASCII, emoji ## 3.2.0 diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index df90e39..8daa94f 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -629,7 +629,7 @@ extension type BinaryWriter._(_WriterState _ws) { // Step 6: Backtrack and write the actual VarInt length final finalOffset = _ws.offset; - _ws.offset = startOffset; + seek(startOffset); writeVarUint(byteLength); _ws.offset = finalOffset; } @@ -720,6 +720,58 @@ extension type BinaryWriter._(_WriterState _ws) { @pragma('dart2js:tryInline') Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); + /// Sets the write position to the specified byte offset. + /// + /// Subsequent writes will start from this new position. + /// Use to go back and overwrite data, or to skip ahead. + /// + /// Throws [RangeError] if [position] is negative or exceeds [bytesWritten]. + /// + /// Example: + /// ```dart + /// writer.writeUint32(42); + /// writer.writeUint32(100); + /// writer.seek(0); // Go back to the beginning + /// writer.writeUint32(99); // Overwrite 42 with 99 + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void seek(int position) { + if (position < 0 || position > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset, 'position'); + } + + _ws.offset = position; + } + + /// Writes a byte at the specified [position] without changing the current + /// write position. + /// + /// This is useful for overwriting data at a known offset (e.g., updating a + /// length field after writing the payload). + /// + /// Throws [RangeError] if [position] is negative or exceeds [bytesWritten]. + /// + /// Example: + /// ```dart + /// writer.writeUint32(10); // Write length placeholder + /// writer.writeString('data'); + /// writer.writeUint8At(0, 15); // Overwrite length at position 0 + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void writeUint8At(int position, int value) { + if (position < 0 || position > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset, 'position'); + } + + _checkRange(value, 0, 255, 'Uint8'); + _ws.list[position] = value; + if (position == _ws.offset) { + _ws.offset = position + 1; + } + } + /// Resets the writer to its initial state, discarding all written data. @pragma('vm:prefer-inline') void reset() => _ws._initializeBuffer(); diff --git a/test/unit/binary_writer_seek_test.dart b/test/unit/binary_writer_seek_test.dart new file mode 100644 index 0000000..7a9dbad --- /dev/null +++ b/test/unit/binary_writer_seek_test.dart @@ -0,0 +1,176 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter.seek', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('seeks to position 0', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.seek(0); + expect(writer.bytesWritten, equals(0)); + expect(writer.toBytes(), isEmpty); + }); + + test('seeks to middle position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3) + ..writeUint8(4); + writer.seek(2); + writer.writeUint8(99); + expect(writer.toBytes(), equals([1, 2, 99])); + }); + + test('seeks to end (bytesWritten)', () { + writer + ..writeUint8(1) + ..writeUint8(2); + writer.seek(2); + writer.writeUint8(3); + expect(writer.toBytes(), equals([1, 2, 3])); + }); + + test('throws RangeError for negative position', () { + expect(() => writer.seek(-1), throwsA(isA())); + }); + + test('throws RangeError for position beyond bytesWritten', () { + writer.writeUint8(1); + expect(() => writer.seek(2), throwsA(isA())); + }); + + test('seek and overwrite at beginning', () { + writer + ..writeUint32(0x11223344) + ..writeUint32(0xAABBCCDD); + writer.seek(0); + writer.writeUint32(0x99887766); + expect(writer.toBytes(), equals([0x99, 0x88, 0x77, 0x66])); + }); + + test('seek preserves bytesWritten after overwrite', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.seek(1); + writer.writeUint8(99); + expect(writer.bytesWritten, equals(2)); + }); + }); + + group('BinaryWriter.writeUint8At', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('overwrites byte at position 0', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.writeUint8At(0, 99); + expect(writer.toBytes(), equals([99, 2, 3])); + }); + + test('overwrites byte at middle position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.writeUint8At(1, 99); + expect(writer.toBytes(), equals([1, 99, 3])); + }); + + test('overwrites byte at last position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.writeUint8At(2, 99); + expect(writer.toBytes(), equals([1, 2, 99])); + }); + + test('does not change bytesWritten', () { + writer + ..writeUint8(1) + ..writeUint8(2); + writer.writeUint8At(0, 99); + expect(writer.bytesWritten, equals(2)); + }); + + test('does not change current write position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + writer.writeUint8At(1, 99); + writer.writeUint8(4); + expect(writer.toBytes(), equals([1, 99, 3, 4])); + }); + + test('throws RangeError for negative position', () { + writer.writeUint8(1); + expect(() => writer.writeUint8At(-1, 0), throwsA(isA())); + }); + + test('throws RangeError for position beyond bytesWritten', () { + writer.writeUint8(1); + expect(() => writer.writeUint8At(2, 0), throwsA(isA())); + }); + + test('throws RangeError for value exceeding 255', () { + expect(() => writer.writeUint8At(0, 256), throwsA(isA())); + }); + + test('throws RangeError for negative value', () { + expect(() => writer.writeUint8At(0, -1), throwsA(isA())); + }); + + test('works on empty writer at position 0', () { + writer.writeUint8At(0, 42); + expect(writer.bytesWritten, equals(1)); + expect(writer.toBytes(), equals([42])); + }); + }); + + group('BinaryWriter.seek + writeVarString integration', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('writeVarString uses seek internally', () { + writer.writeVarString('hello'); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('hello')); + }); + + test('writeVarString with non-ASCII uses seek for VarInt rewrite', () { + writer.writeVarString('Привет'); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('Привет')); + }); + + test('writeVarString with emoji uses seek for VarInt rewrite', () { + writer.writeVarString('🚀🌍'); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('🚀🌍')); + }); + }); +} From c09a4615040c91fa14c3b70fa0450c39dca0a318 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 09:50:29 +0300 Subject: [PATCH 03/17] fix: warnings + dart format . --- lib/src/binary_reader.dart | 3 +- lib/src/binary_writer.dart | 20 +++++------ test/unit/binary_writer_seek_test.dart | 50 +++++++++++++------------- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 9b6b230..106f63c 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -680,14 +680,13 @@ 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. + /// After rebinding, the reader starts at position 0 of the new buffer. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void rebind(Uint8List buffer) { diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 8daa94f..2311a52 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -761,16 +761,16 @@ extension type BinaryWriter._(_WriterState _ws) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void writeUint8At(int position, int value) { - if (position < 0 || position > _ws.offset) { - throw RangeError.range(position, 0, _ws.offset, 'position'); - } - - _checkRange(value, 0, 255, 'Uint8'); - _ws.list[position] = value; - if (position == _ws.offset) { - _ws.offset = position + 1; - } - } + if (position < 0 || position > _ws.offset) { + throw RangeError.range(position, 0, _ws.offset, 'position'); + } + + _checkRange(value, 0, 255, 'Uint8'); + _ws.list[position] = value; + if (position == _ws.offset) { + _ws.offset = position + 1; + } + } /// Resets the writer to its initial state, discarding all written data. @pragma('vm:prefer-inline') diff --git a/test/unit/binary_writer_seek_test.dart b/test/unit/binary_writer_seek_test.dart index 7a9dbad..21459d5 100644 --- a/test/unit/binary_writer_seek_test.dart +++ b/test/unit/binary_writer_seek_test.dart @@ -13,8 +13,8 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.seek(0); + ..writeUint8(3) + ..seek(0); expect(writer.bytesWritten, equals(0)); expect(writer.toBytes(), isEmpty); }); @@ -24,18 +24,18 @@ void main() { ..writeUint8(1) ..writeUint8(2) ..writeUint8(3) - ..writeUint8(4); - writer.seek(2); - writer.writeUint8(99); + ..writeUint8(4) + ..seek(2) + ..writeUint8(99); expect(writer.toBytes(), equals([1, 2, 99])); }); test('seeks to end (bytesWritten)', () { writer ..writeUint8(1) - ..writeUint8(2); - writer.seek(2); - writer.writeUint8(3); + ..writeUint8(2) + ..seek(2) + ..writeUint8(3); expect(writer.toBytes(), equals([1, 2, 3])); }); @@ -51,9 +51,9 @@ void main() { test('seek and overwrite at beginning', () { writer ..writeUint32(0x11223344) - ..writeUint32(0xAABBCCDD); - writer.seek(0); - writer.writeUint32(0x99887766); + ..writeUint32(0xAABBCCDD) + ..seek(0) + ..writeUint32(0x99887766); expect(writer.toBytes(), equals([0x99, 0x88, 0x77, 0x66])); }); @@ -61,9 +61,9 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.seek(1); - writer.writeUint8(99); + ..writeUint8(3) + ..seek(1) + ..writeUint8(99); expect(writer.bytesWritten, equals(2)); }); }); @@ -79,8 +79,8 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.writeUint8At(0, 99); + ..writeUint8(3) + ..writeUint8At(0, 99); expect(writer.toBytes(), equals([99, 2, 3])); }); @@ -88,8 +88,8 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.writeUint8At(1, 99); + ..writeUint8(3) + ..writeUint8At(1, 99); expect(writer.toBytes(), equals([1, 99, 3])); }); @@ -97,16 +97,16 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.writeUint8At(2, 99); + ..writeUint8(3) + ..writeUint8At(2, 99); expect(writer.toBytes(), equals([1, 2, 99])); }); test('does not change bytesWritten', () { writer ..writeUint8(1) - ..writeUint8(2); - writer.writeUint8At(0, 99); + ..writeUint8(2) + ..writeUint8At(0, 99); expect(writer.bytesWritten, equals(2)); }); @@ -114,9 +114,9 @@ void main() { writer ..writeUint8(1) ..writeUint8(2) - ..writeUint8(3); - writer.writeUint8At(1, 99); - writer.writeUint8(4); + ..writeUint8(3) + ..writeUint8At(1, 99) + ..writeUint8(4); expect(writer.toBytes(), equals([1, 99, 3, 4])); }); From e56bdfb0aae66504f5e077e5f56a6b7c1a89c5b3 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 10:50:59 +0300 Subject: [PATCH 04/17] separate tests --- .../advanced_features_integration_test.dart | 43 + test/integration/basic_integration_test.dart | 56 + .../complex_structures_integration_test.dart | 58 + test/integration/integration_test.dart | 1375 -------- .../robustness_integration_test.dart | 49 + test/integration/types_integration_test.dart | 72 + test/unit/binary_reader_basic_test.dart | 93 + test/unit/binary_reader_buffer_test.dart | 73 + test/unit/binary_reader_edge_cases_test.dart | 78 + test/unit/binary_reader_navigation_test.dart | 67 + test/unit/binary_reader_string_test.dart | 53 + test/unit/binary_reader_test.dart | 2194 ------------ test/unit/binary_reader_var_types_test.dart | 80 + test/unit/binary_writer_basic_test.dart | 186 ++ test/unit/binary_writer_buffer_test.dart | 212 ++ test/unit/binary_writer_complex_test.dart | 81 + test/unit/binary_writer_edge_cases_test.dart | 93 + test/unit/binary_writer_navigation_test.dart | 58 + test/unit/binary_writer_pool_test.dart | 82 + test/unit/binary_writer_seek_test.dart | 176 - test/unit/binary_writer_string_test.dart | 312 ++ test/unit/binary_writer_test.dart | 2931 ----------------- test/unit/binary_writer_var_types_test.dart | 144 + 23 files changed, 1890 insertions(+), 6676 deletions(-) create mode 100644 test/integration/advanced_features_integration_test.dart create mode 100644 test/integration/basic_integration_test.dart create mode 100644 test/integration/complex_structures_integration_test.dart delete mode 100644 test/integration/integration_test.dart create mode 100644 test/integration/robustness_integration_test.dart create mode 100644 test/integration/types_integration_test.dart create mode 100644 test/unit/binary_reader_basic_test.dart create mode 100644 test/unit/binary_reader_buffer_test.dart create mode 100644 test/unit/binary_reader_edge_cases_test.dart create mode 100644 test/unit/binary_reader_navigation_test.dart create mode 100644 test/unit/binary_reader_string_test.dart delete mode 100644 test/unit/binary_reader_test.dart create mode 100644 test/unit/binary_reader_var_types_test.dart create mode 100644 test/unit/binary_writer_basic_test.dart create mode 100644 test/unit/binary_writer_buffer_test.dart create mode 100644 test/unit/binary_writer_complex_test.dart create mode 100644 test/unit/binary_writer_edge_cases_test.dart create mode 100644 test/unit/binary_writer_navigation_test.dart create mode 100644 test/unit/binary_writer_pool_test.dart delete mode 100644 test/unit/binary_writer_seek_test.dart create mode 100644 test/unit/binary_writer_string_test.dart delete mode 100644 test/unit/binary_writer_test.dart create mode 100644 test/unit/binary_writer_var_types_test.dart diff --git a/test/integration/advanced_features_integration_test.dart b/test/integration/advanced_features_integration_test.dart new file mode 100644 index 0000000..bc4146d --- /dev/null +++ b/test/integration/advanced_features_integration_test.dart @@ -0,0 +1,43 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('Advanced Features Integration Tests', () { + test('reader navigation after complex write', () { + final writer = BinaryWriter() + ..writeUint32(1) + ..writeUint32(2) + ..writeUint32(3); + final reader = BinaryReader(writer.takeBytes())..seek(4); + expect(reader.readUint32(), equals(2)); + + reader.rewind(4); + expect(reader.readUint32(), equals(2)); + }); + + test('multiple readers on same data', () { + final writer = BinaryWriter() + ..writeUint32(100) + ..writeUint32(200); + final bytes = writer.takeBytes(); + + final reader1 = BinaryReader(bytes); + final reader2 = BinaryReader(bytes); + + expect(reader1.readUint32(), equals(100)); + expect(reader2.readUint32(), equals(100)); + expect(reader1.readUint32(), equals(200)); + expect(reader2.readUint32(), equals(200)); + }); + + test('writer buffer management - toBytes preserves state', () { + final writer = BinaryWriter()..writeUint32(100); + final bytes1 = writer.toBytes(); + expect(bytes1, equals([0, 0, 0, 100])); + + writer.writeUint32(200); + final bytes2 = writer.toBytes(); + expect(bytes2, equals([0, 0, 0, 100, 0, 0, 0, 200])); + }); + }); +} diff --git a/test/integration/basic_integration_test.dart b/test/integration/basic_integration_test.dart new file mode 100644 index 0000000..559b9c6 --- /dev/null +++ b/test/integration/basic_integration_test.dart @@ -0,0 +1,56 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('Basic Integration Tests', () { + test('write and read single Uint8', () { + final writer = BinaryWriter()..writeUint8(42); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint8(), equals(42)); + }); + + test('write and read single Int8', () { + final writer = BinaryWriter()..writeInt8(-42); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readInt8(), equals(-42)); + }); + + test('write and read Uint16 with big-endian', () { + final writer = BinaryWriter()..writeUint16(65535); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint16(), equals(65535)); + }); + + test('write and read Float32', () { + final writer = BinaryWriter()..writeFloat32(3.14159); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readFloat32(), closeTo(3.14159, 0.00001)); + }); + + test('write and read basic cycles for all fixed types', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeInt8(-1) + ..writeUint16(1000) + ..writeInt16(-1000) + ..writeUint32(100000) + ..writeInt32(-100000) + ..writeUint64(1000000000) + ..writeInt64(-1000000000) + ..writeFloat32(1.5) + ..writeFloat64(2.718); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint8(), equals(1)); + expect(reader.readInt8(), equals(-1)); + expect(reader.readUint16(), equals(1000)); + expect(reader.readInt16(), equals(-1000)); + expect(reader.readUint32(), equals(100000)); + expect(reader.readInt32(), equals(-100000)); + expect(reader.readUint64(), equals(1000000000)); + expect(reader.readInt64(), equals(-1000000000)); + expect(reader.readFloat32(), equals(1.5)); + expect(reader.readFloat64(), equals(2.718)); + }); + }); +} diff --git a/test/integration/complex_structures_integration_test.dart b/test/integration/complex_structures_integration_test.dart new file mode 100644 index 0000000..f36579a --- /dev/null +++ b/test/integration/complex_structures_integration_test.dart @@ -0,0 +1,58 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('Complex Structures Integration Tests', () { + test('write and read sequence of different types', () { + final writer = BinaryWriter() + ..writeUint8(255) + ..writeInt8(-128) + ..writeUint16(65535) + ..writeInt16(-32768) + ..writeUint32(4294967295) + ..writeInt32(-2147483648) + ..writeUint64(9223372036854775807) + ..writeInt64(-9223372036854775808) + ..writeFloat32(1.5) + ..writeFloat64(2.718281828); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint8(), equals(255)); + expect(reader.readInt8(), equals(-128)); + expect(reader.readUint16(), equals(65535)); + expect(reader.readInt16(), equals(-32768)); + expect(reader.readUint32(), equals(4294967295)); + expect(reader.readInt32(), equals(-2147483648)); + expect(reader.readUint64(), equals(9223372036854775807)); + expect(reader.readInt64(), equals(-9223372036854775808)); + expect(reader.readFloat32(), closeTo(1.5, 0.01)); + expect(reader.readFloat64(), closeTo(2.718281828, 0.000000001)); + }); + + group('Real-world message format simulation', () { + test('protocol with header and payload', () { + final writer = BinaryWriter() + // Header + ..writeUint8(1) // version + ..writeUint8(42) // message type + ..writeUint32(123456) // message id + // Payload + ..writeVarString('user@example.com') + ..writeVarUint(1000) + ..writeBool(true); + + final reader = BinaryReader(writer.takeBytes()); + + // Read header + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(42)); + expect(reader.readUint32(), equals(123456)); + + // Read payload + expect(reader.readVarString(), equals('user@example.com')); + expect(reader.readVarUint(), equals(1000)); + expect(reader.readBool(), isTrue); + }); + }); + }); +} diff --git a/test/integration/integration_test.dart b/test/integration/integration_test.dart deleted file mode 100644 index ecf4fa9..0000000 --- a/test/integration/integration_test.dart +++ /dev/null @@ -1,1375 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -void main() { - group('Integration Tests - BinaryReader and BinaryWriter', () { - group('Basic read-write cycles', () { - test('write and read single Uint8', () { - final writer = BinaryWriter(); - const value = 42; - - writer.writeUint8(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint8(), equals(value)); - expect(reader.availableBytes, equals(0)); - }); - - test('write and read single Int8', () { - final writer = BinaryWriter(); - const value = -42; - - writer.writeInt8(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt8(), equals(value)); - }); - - test('write and read Uint16 with big-endian', () { - final writer = BinaryWriter(); - const value = 65535; - - writer.writeUint16(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint16(), equals(value)); - }); - - test('write and read Uint16 with little-endian', () { - final writer = BinaryWriter(); - const value = 65535; - - writer.writeUint16(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint16(.little), equals(value)); - }); - - test('write and read Int16 with big-endian', () { - final writer = BinaryWriter(); - const value = -32768; - - writer.writeInt16(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt16(), equals(value)); - }); - - test('write and read Int16 with little-endian', () { - final writer = BinaryWriter(); - const value = -32768; - - writer.writeInt16(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt16(.little), equals(value)); - }); - - test('write and read Uint32 with big-endian', () { - final writer = BinaryWriter(); - const value = 4294967295; - - writer.writeUint32(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint32(), equals(value)); - }); - - test('write and read Uint32 with little-endian', () { - final writer = BinaryWriter(); - const value = 4294967295; - - writer.writeUint32(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint32(.little), equals(value)); - }); - - test('write and read Int32 with big-endian', () { - final writer = BinaryWriter(); - const value = -2147483648; - - writer.writeInt32(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt32(), equals(value)); - }); - - test('write and read Int32 with little-endian', () { - final writer = BinaryWriter(); - const value = -2147483648; - - writer.writeInt32(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt32(.little), equals(value)); - }); - - test('write and read Uint64 with big-endian', () { - final writer = BinaryWriter(); - const value = 9223372036854775807; - - writer.writeUint64(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint64(), equals(value)); - }); - - test('write and read Uint64 with little-endian', () { - final writer = BinaryWriter(); - const value = 9223372036854775807; - - writer.writeUint64(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint64(.little), equals(value)); - }); - - test('write and read Int64 with big-endian', () { - final writer = BinaryWriter(); - const value = -9223372036854775808; - - writer.writeInt64(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt64(), equals(value)); - }); - - test('write and read Int64 with little-endian', () { - final writer = BinaryWriter(); - const value = -9223372036854775808; - - writer.writeInt64(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readInt64(.little), equals(value)); - }); - - test('write and read Float32 with big-endian', () { - final writer = BinaryWriter(); - const value = 3.14159; - - writer.writeFloat32(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), closeTo(value, 0.00001)); - }); - - test('write and read Float32 with little-endian', () { - final writer = BinaryWriter(); - const value = 3.14159; - - writer.writeFloat32(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(.little), closeTo(value, 0.00001)); - }); - - test('write and read Float64 with big-endian', () { - final writer = BinaryWriter(); - const value = 3.141592653589793; - - writer.writeFloat64(value); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), closeTo(value, 0.000000000000001)); - }); - - test('write and read Float64 with little-endian', () { - final writer = BinaryWriter(); - const value = 3.141592653589793; - - writer.writeFloat64(value, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect( - reader.readFloat64(.little), - closeTo(value, 0.000000000000001), - ); - }); - }); - - group('Complex data structures', () { - test('write and read sequence of different types', () { - final writer = BinaryWriter() - ..writeUint8(255) - ..writeInt8(-128) - ..writeUint16(65535) - ..writeInt16(-32768) - ..writeUint32(4294967295) - ..writeInt32(-2147483648) - ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808) - ..writeFloat32(1.5) - ..writeFloat64(2.718281828); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(255)); - expect(reader.readInt8(), equals(-128)); - expect(reader.readUint16(), equals(65535)); - expect(reader.readInt16(), equals(-32768)); - expect(reader.readUint32(), equals(4294967295)); - expect(reader.readInt32(), equals(-2147483648)); - expect(reader.readUint64(), equals(9223372036854775807)); - expect(reader.readInt64(), equals(-9223372036854775808)); - expect(reader.readFloat32(), closeTo(1.5, 0.01)); - expect(reader.readFloat64(), closeTo(2.718281828, 0.000000001)); - expect(reader.availableBytes, equals(0)); - }); - - test('write and read with mixed endianness', () { - final writer = BinaryWriter() - ..writeUint16(0x1234) - ..writeUint16(0x5678, .little) - ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, .little) - ..writeFloat32(3.14) - ..writeFloat32(2.71, .little) - ..writeFloat64(1.414) - ..writeFloat64(1.732, .little); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(.little), equals(0x5678)); - expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(.little), equals(0x11223344)); - expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); - expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); - }); - - test('write and read bytes array', () { - final writer = BinaryWriter(); - final data = Uint8List.fromList([1, 2, 3, 4, 5, 100, 200, 255]); - - writer.writeBytes(data); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final readData = reader.readBytes(data.length); - - expect(readData, equals(data)); - expect(reader.availableBytes, equals(0)); - }); - - test('write and read string in UTF-8', () { - final writer = BinaryWriter(); - const str = 'Hello, World!'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - - expect(result, equals(str)); - }); - - test('write and read multi-byte UTF-8 string', () { - final writer = BinaryWriter(); - const str = 'Привет, мир! 你好世界 🚀'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - - expect(result, equals(str)); - }); - - test('write and read alternating string and numeric data', () { - final writer = BinaryWriter() - ..writeString('Start') - ..writeUint32(42) - ..writeString('Middle') - ..writeFloat64(3.14159) - ..writeString('End'); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readString(5), equals('Start')); - expect(reader.readUint32(), equals(42)); - expect(reader.readString(6), equals('Middle')); - expect(reader.readFloat64(), closeTo(3.14159, 0.00001)); - expect(reader.readString(3), equals('End')); - expect(reader.availableBytes, equals(0)); - }); - - test('write and read nested numeric values', () { - final writer = BinaryWriter(); - final values = [ - 255, - 127, - 65535, - 32767, - 4294967295, - 2147483647, - 9223372036854775807, - ]; - - for (final _ in values) { - writer - ..writeUint8(255) - ..writeInt8(127) - ..writeUint16(65535) - ..writeInt16(32767) - ..writeUint32(4294967295) - ..writeInt32(2147483647) - ..writeUint64(9223372036854775807); - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - for (var i = 0; i < values.length; i++) { - expect(reader.readUint8(), equals(255)); - expect(reader.readInt8(), equals(127)); - expect(reader.readUint16(), equals(65535)); - expect(reader.readInt16(), equals(32767)); - expect(reader.readUint32(), equals(4294967295)); - expect(reader.readInt32(), equals(2147483647)); - expect(reader.readUint64(), equals(9223372036854775807)); - } - - expect(reader.availableBytes, equals(0)); - }); - }); - - group('String handling integration', () { - test('write and read ASCII strings', () { - final writer = BinaryWriter(); - const str = 'ASCII123!@#'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('write and read Cyrillic strings', () { - final writer = BinaryWriter(); - const str = 'Привет, мир!'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('write and read Chinese characters', () { - final writer = BinaryWriter(); - const str = '你好世界'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('write and read emoji characters', () { - final writer = BinaryWriter(); - const str = '🚀🌟💻🎉🔥'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('write and read mixed Unicode string', () { - final writer = BinaryWriter(); - const str = 'ASCII_Юникод_中文_🌍'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('write and read empty string', () { - final writer = BinaryWriter()..writeString(''); - final bytes = writer.takeBytes(); - - expect(bytes, isEmpty); - }); - - test('write multiple strings and read them back', () { - final writer = BinaryWriter(); - final strings = ['Hello', 'Привет', '你好', '🌍'] - ..forEach(writer.writeString); - - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - for (final str in strings) { - final strBytes = utf8.encode(str); - final readStr = reader.readString(strBytes.length); - expect(readStr, equals(str)); - } - - expect(reader.availableBytes, equals(0)); - }); - }); - - group('Float special values', () { - test('write and read Float32 NaN', () { - final writer = BinaryWriter()..writeFloat32(.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('write and read Float32 positive Infinity', () { - final writer = BinaryWriter()..writeFloat32(.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.infinity)); - }); - - test('write and read Float32 negative Infinity', () { - final writer = BinaryWriter()..writeFloat32(.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.negativeInfinity)); - }); - - test('write and read Float64 NaN', () { - final writer = BinaryWriter()..writeFloat64(.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64().isNaN, isTrue); - }); - - test('write and read Float64 positive Infinity', () { - final writer = BinaryWriter()..writeFloat64(.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.infinity)); - }); - - test('write and read Float64 negative Infinity', () { - final writer = BinaryWriter()..writeFloat64(.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }); - - test('write and read Float64 negative zero', () { - final writer = BinaryWriter()..writeFloat64(-0); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final value = reader.readFloat64(); - expect(value, equals(0.0)); - expect(value.isNegative, isTrue); - }); - - test( - 'write and read multiple special float values together', - () { - final writer = BinaryWriter() - ..writeFloat32(.nan) - ..writeFloat32(.infinity) - ..writeFloat32(.negativeInfinity) - ..writeFloat64(.nan) - ..writeFloat64(.infinity) - ..writeFloat64(.negativeInfinity); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readFloat32().isNaN, isTrue); - expect(reader.readFloat32(), equals(double.infinity)); - expect(reader.readFloat32(), equals(double.negativeInfinity)); - expect(reader.readFloat64().isNaN, isTrue); - expect(reader.readFloat64(), equals(double.infinity)); - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }, - ); - }); - - group('Reader operations after write', () { - test('peek and then read same bytes', () { - final writer = BinaryWriter()..writeBytes([1, 2, 3, 4, 5]); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final peeked = reader.peekBytes(3); - final read = reader.readBytes(3); - - expect(peeked, equals(read)); - expect(peeked, equals([1, 2, 3])); - }); - - test('skip and then read remaining bytes', () { - final writer = BinaryWriter() - ..writeBytes([1, 2, 3, 4, 5]) - ..writeUint32(42); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes)..skip(5); - expect(reader.readUint32(), equals(42)); - }); - - test('offset tracking during read', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint16(2) - ..writeUint32(3); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.offset, equals(0)); - reader.readUint8(); - expect(reader.offset, equals(1)); - reader.readUint16(); - expect(reader.offset, equals(3)); - reader.readUint32(); - expect(reader.offset, equals(7)); - }); - - test('availableBytes tracking', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8(4); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.availableBytes, equals(4)); - reader.readUint8(); - expect(reader.availableBytes, equals(3)); - reader.readUint8(); - expect(reader.availableBytes, equals(2)); - reader.readUint16(); - expect(reader.availableBytes, equals(0)); - }); - }); - - group('Large data cycles', () { - test('write and read large byte array', () { - const size = 100000; - final writer = BinaryWriter(); - final data = Uint8List(size); - for (var i = 0; i < size; i++) { - data[i] = i % 256; - } - - writer.writeBytes(data); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final readData = reader.readBytes(size); - - expect(readData.length, equals(size)); - for (var i = 0; i < size; i++) { - expect(readData[i], equals(i % 256)); - } - }); - - test('write and read many numeric values', () { - const count = 1000; - final writer = BinaryWriter(); - - for (var i = 0; i < count; i++) { - writer - ..writeUint8(i % 256) - ..writeUint16(i * 2) - ..writeUint32(i * 1000); - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - for (var i = 0; i < count; i++) { - expect(reader.readUint8(), equals(i % 256)); - expect(reader.readUint16(), equals(i * 2)); - expect(reader.readUint32(), equals(i * 1000)); - } - - expect(reader.availableBytes, equals(0)); - }); - - test('write and read very long string', () { - final writer = BinaryWriter(); - final longString = 'A' * 50000; - - writer.writeString(longString); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - - expect(result, equals(longString)); - expect(result.length, equals(50000)); - }); - }); - - group('Writer buffer management integration', () { - test('buffer expansion preserves data integrity', () { - final writer = BinaryWriter(initialBufferSize: 4); - - for (var i = 0; i < 100; i++) { - writer.writeUint8(i % 256); - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - for (var i = 0; i < 100; i++) { - expect(reader.readUint8(), equals(i % 256)); - } - }); - - test('use toBytes and continue writing', () { - final writer = BinaryWriter()..writeUint32(100); - - final bytes1 = writer.toBytes(); - expect(bytes1.length, equals(4)); - - writer.writeUint32(200); - final bytes2 = writer.toBytes(); - expect(bytes2.length, equals(8)); - - final reader = BinaryReader(bytes2); - expect(reader.readUint32(), equals(100)); - expect(reader.readUint32(), equals(200)); - }); - - test('takeBytes resets and new writes start fresh', () { - final writer = BinaryWriter()..writeUint32(100); - final bytes1 = writer.takeBytes(); - - expect(bytes1.length, equals(4)); - - writer.writeUint32(200); - final bytes2 = writer.takeBytes(); - - expect(bytes2.length, equals(4)); - - final reader = BinaryReader(bytes2); - expect(reader.readUint32(), equals(200)); - }); - - test('reset clears buffer and can write new data', () { - final writer = BinaryWriter() - ..writeUint32(100) - ..reset() - ..writeUint32(200); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint32(), equals(200)); - }); - }); - - group('Round-trip validation', () { - test( - 'all types round-trip correctly with big-endian', - () { - final writer = BinaryWriter() - ..writeUint8(255) - ..writeInt8(-128) - ..writeUint16(65535) - ..writeInt16(-32768) - ..writeUint32(4294967295) - ..writeInt32(-2147483648) - ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808) - ..writeFloat32(1.23456) - ..writeFloat64(1.2345678901234) - ..writeString('Test') - ..writeBytes([1, 2, 3]); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(255)); - expect(reader.readInt8(), equals(-128)); - expect(reader.readUint16(), equals(65535)); - expect(reader.readInt16(), equals(-32768)); - expect(reader.readUint32(), equals(4294967295)); - expect(reader.readInt32(), equals(-2147483648)); - expect(reader.readUint64(), equals(9223372036854775807)); - expect(reader.readInt64(), equals(-9223372036854775808)); - expect(reader.readFloat32(), closeTo(1.23456, 0.00001)); - expect(reader.readFloat64(), closeTo(1.2345678901234, 0.0000001)); - expect(reader.readString(4), equals('Test')); - expect(reader.readBytes(3), equals([1, 2, 3])); - expect(reader.availableBytes, equals(0)); - }, - ); - - test( - 'all types round-trip correctly with little-endian', - () { - final writer = BinaryWriter() - ..writeUint16(65535, .little) - ..writeInt16(-32768, .little) - ..writeUint32(4294967295, .little) - ..writeInt32(-2147483648, .little) - ..writeUint64(9223372036854775807, .little) - ..writeInt64(-9223372036854775808, .little) - ..writeFloat32(1.23456, .little) - ..writeFloat64(1.2345678901234, .little); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect( - reader.readUint16(.little), - equals(65535), - ); - expect( - reader.readInt16(.little), - equals(-32768), - ); - expect( - reader.readUint32(.little), - equals(4294967295), - ); - expect( - reader.readInt32(.little), - equals(-2147483648), - ); - expect( - reader.readUint64(.little), - equals(9223372036854775807), - ); - expect( - reader.readInt64(.little), - equals(-9223372036854775808), - ); - expect( - reader.readFloat32(.little), - closeTo(1.23456, 0.00001), - ); - expect( - reader.readFloat64(.little), - closeTo(1.2345678901234, 0.0000001), - ); - expect(reader.availableBytes, equals(0)); - }, - ); - }); - - group('Boundary condition integration', () { - test('write and read exactly to buffer boundary', () { - final writer = BinaryWriter() - ..writeUint16(0x0102) - ..writeUint16(0x0304) - ..writeUint16(0x0506); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint16(), equals(0x0102)); - expect(reader.readUint16(), equals(0x0304)); - expect(reader.readUint16(), equals(0x0506)); - expect(reader.availableBytes, equals(0)); - }); - - test('write zero-length data and read correctly', () { - final writer = BinaryWriter() - ..writeUint8(42) - ..writeBytes([]) - ..writeUint8(43); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(42)); - expect(reader.readUint8(), equals(43)); - }); - - test('write empty string between numeric values', () { - final writer = BinaryWriter() - ..writeUint32(100) - ..writeString('') - ..writeUint32(200); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint32(), equals(100)); - expect(reader.readUint32(), equals(200)); - }); - }); - - group('Multiple reader instances on same data', () { - test('multiple readers read same buffer independently', () { - final writer = BinaryWriter() - ..writeUint32(100) - ..writeString('Hello') - ..writeUint32(200); - - final bytes = writer.takeBytes(); - - final reader1 = BinaryReader(bytes); - final reader2 = BinaryReader(bytes); - - expect(reader1.readUint32(), equals(100)); - expect(reader2.readUint32(), equals(100)); - - final str1 = reader1.readString(5); - reader2.skip(5); - - expect(str1, equals('Hello')); - expect(reader1.readUint32(), equals(200)); - expect(reader2.readUint32(), equals(200)); - }); - }); - - group('Stress tests', () { - test('alternating write and read operations', () { - final writer = BinaryWriter(); - - for (var i = 0; i < 100; i++) { - writer - ..writeUint16(i * 2) - ..writeString('Item$i'); - } - - final bytes = writer.takeBytes(); - final newReader = BinaryReader(bytes); - - for (var i = 0; i < 100; i++) { - expect(newReader.readUint16(), equals(i * 2)); - final itemStr = 'Item$i'; - expect(newReader.readString(itemStr.length), equals(itemStr)); - } - }); - - test('recursive-like nested structures', () { - final writer = BinaryWriter(); - - // Simulate nested structures - for (var i = 0; i < 10; i++) { - writer.writeUint8(i); - for (var j = 0; j < 5; j++) { - writer.writeUint16(i * 100 + j); - } - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - for (var i = 0; i < 10; i++) { - expect(reader.readUint8(), equals(i)); - for (var j = 0; j < 5; j++) { - expect(reader.readUint16(), equals(i * 100 + j)); - } - } - }); - }); - - group('Variable-length integer operations', () { - test('write and read VarUint single byte', () { - final writer = BinaryWriter()..writeVarUint(127); - final bytes = writer.takeBytes(); - - expect(bytes.length, equals(1)); - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(127)); - }); - - test('write and read VarUint two bytes', () { - final writer = BinaryWriter()..writeVarUint(300); - final bytes = writer.takeBytes(); - - expect(bytes.length, equals(2)); - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(300)); - }); - - test('write and read VarUint large value', () { - final writer = BinaryWriter()..writeVarUint(1000000); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(1000000)); - }); - - test('write and read multiple VarUints', () { - final writer = BinaryWriter() - ..writeVarUint(0) - ..writeVarUint(127) - ..writeVarUint(128) - ..writeVarUint(16383) - ..writeVarUint(16384) - ..writeVarUint(2097151); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(0)); - expect(reader.readVarUint(), equals(127)); - expect(reader.readVarUint(), equals(128)); - expect(reader.readVarUint(), equals(16383)); - expect(reader.readVarUint(), equals(16384)); - expect(reader.readVarUint(), equals(2097151)); - }); - - test('write and read VarInt positive values', () { - final writer = BinaryWriter() - ..writeVarInt(0) - ..writeVarInt(42) - ..writeVarInt(1000); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarInt(), equals(0)); - expect(reader.readVarInt(), equals(42)); - expect(reader.readVarInt(), equals(1000)); - }); - - test('write and read VarInt negative values', () { - final writer = BinaryWriter() - ..writeVarInt(-1) - ..writeVarInt(-64) - ..writeVarInt(-1000); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarInt(), equals(-1)); - expect(reader.readVarInt(), equals(-64)); - expect(reader.readVarInt(), equals(-1000)); - }); - - test('write and read VarInt mixed positive and negative', () { - final writer = BinaryWriter() - ..writeVarInt(-1) - ..writeVarInt(1) - ..writeVarInt(-100) - ..writeVarInt(100) - ..writeVarInt(-10000) - ..writeVarInt(10000); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarInt(), equals(-1)); - expect(reader.readVarInt(), equals(1)); - expect(reader.readVarInt(), equals(-100)); - expect(reader.readVarInt(), equals(100)); - expect(reader.readVarInt(), equals(-10000)); - expect(reader.readVarInt(), equals(10000)); - }); - }); - - group('Boolean operations', () { - test('write and read single boolean true', () { - final writer = BinaryWriter()..writeBool(true); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readBool(), isTrue); - }); - - test('write and read single boolean false', () { - final writer = BinaryWriter()..writeBool(false); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readBool(), isFalse); - }); - - test('write and read multiple booleans', () { - final writer = BinaryWriter() - ..writeBool(true) - ..writeBool(false) - ..writeBool(true) - ..writeBool(true) - ..writeBool(false); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - }); - - test('write and read booleans mixed with other types', () { - final writer = BinaryWriter() - ..writeBool(true) - ..writeUint32(42) - ..writeBool(false) - ..writeString('test') - ..writeBool(true); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readBool(), isTrue); - expect(reader.readUint32(), equals(42)); - expect(reader.readBool(), isFalse); - expect(reader.readString(4), equals('test')); - expect(reader.readBool(), isTrue); - }); - }); - - group('VarBytes operations', () { - test('write and read VarBytes empty array', () { - final writer = BinaryWriter()..writeVarBytes([]); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readVarBytes(); - - expect(result, isEmpty); - }); - - test('write and read VarBytes small array', () { - final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4, 5]); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readVarBytes(); - - expect(result, equals([1, 2, 3, 4, 5])); - }); - - test('write and read VarBytes large array', () { - final data = List.generate(1000, (i) => i % 256); - final writer = BinaryWriter()..writeVarBytes(data); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readVarBytes(); - - expect(result, equals(data)); - }); - - test('write and read multiple VarBytes', () { - final writer = BinaryWriter() - ..writeVarBytes([1, 2, 3]) - ..writeVarBytes([4, 5]) - ..writeVarBytes([6, 7, 8, 9]); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarBytes(), equals([1, 2, 3])); - expect(reader.readVarBytes(), equals([4, 5])); - expect(reader.readVarBytes(), equals([6, 7, 8, 9])); - }); - }); - - group('VarString operations', () { - test('write and read VarString empty', () { - final writer = BinaryWriter()..writeVarString(''); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('')); - }); - - test('write and read VarString ASCII', () { - final writer = BinaryWriter()..writeVarString('Hello, World!'); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('Hello, World!')); - }); - - test('write and read VarString UTF-8', () { - final writer = BinaryWriter()..writeVarString('Привет, мир! 你好世界 🚀'); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('Привет, мир! 你好世界 🚀')); - }); - - test('write and read multiple VarStrings', () { - final writer = BinaryWriter() - ..writeVarString('First') - ..writeVarString('Second') - ..writeVarString('Third'); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarString(), equals('First')); - expect(reader.readVarString(), equals('Second')); - expect(reader.readVarString(), equals('Third')); - }); - - test('write and read VarString mixed with other types', () { - final writer = BinaryWriter() - ..writeVarString('Start') - ..writeUint32(42) - ..writeVarString('Middle') - ..writeVarUint(100) - ..writeVarString('End'); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarString(), equals('Start')); - expect(reader.readUint32(), equals(42)); - expect(reader.readVarString(), equals('Middle')); - expect(reader.readVarUint(), equals(100)); - expect(reader.readVarString(), equals('End')); - }); - }); - - group('Reader navigation operations', () { - test('seek to specific position and read', () { - final writer = BinaryWriter() - ..writeUint32(100) - ..writeUint32(200) - ..writeUint32(300); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes)..seek(4); - expect(reader.readUint32(), equals(200)); - - reader.seek(0); - expect(reader.readUint32(), equals(100)); - - reader.seek(8); - expect(reader.readUint32(), equals(300)); - }); - - test('rewind and re-read data', () { - final writer = BinaryWriter() - ..writeUint32(42) - ..writeUint32(84); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - final first = reader.readUint32(); - expect(first, equals(42)); - - reader.rewind(4); - expect(reader.readUint32(), equals(42)); - }); - - test('hasBytes checks availability correctly', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint16(2) - ..writeUint32(3); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.hasBytes(7), isTrue); - expect(reader.hasBytes(8), isFalse); - - reader.readUint8(); - expect(reader.hasBytes(6), isTrue); - expect(reader.hasBytes(7), isFalse); - }); - - test('readRemainingBytes reads all remaining data', () { - final writer = BinaryWriter() - ..writeUint32(42) - ..writeBytes([1, 2, 3, 4, 5]); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes)..readUint32(); - final remaining = reader.readRemainingBytes(); - - expect(remaining, equals([1, 2, 3, 4, 5])); - expect(reader.availableBytes, equals(0)); - }); - - test('combined navigation operations', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8(4) - ..writeUint8(5); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(1)); - reader.skip(2); - expect(reader.readUint8(), equals(4)); - reader.rewind(2); - expect(reader.readUint8(), equals(3)); - reader.seek(0); - expect(reader.readUint8(), equals(1)); - }); - }); - - group('Advanced writer features', () { - test('bytesWritten tracks written data correctly', () { - final writer = BinaryWriter(); - expect(writer.bytesWritten, equals(0)); - - writer.writeUint8(42); - expect(writer.bytesWritten, equals(1)); - - writer.writeUint32(100); - expect(writer.bytesWritten, equals(5)); - - writer.writeString('Hello'); - expect(writer.bytesWritten, equals(10)); - }); - - test('writeBytes with offset and length', () { - final data = Uint8List.fromList([10, 20, 30, 40, 50, 60]); - final writer = BinaryWriter()..writeBytes(data, 2, 3); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readBytes(3), equals([30, 40, 50])); - }); - - test('writeBytes with offset only', () { - final data = Uint8List.fromList([10, 20, 30, 40, 50]); - final writer = BinaryWriter()..writeBytes(data, 2); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readBytes(3), equals([30, 40, 50])); - }); - }); - - group('Real-world message format simulation', () { - test('length-prefixed message with VarInt', () { - final writer = BinaryWriter(); - const message = 'This is a test message'; - final messageBytes = utf8.encode(message); - - writer - ..writeVarUint(messageBytes.length) - ..writeBytes(messageBytes); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - final length = reader.readVarUint(); - final receivedMessage = reader.readString(length); - - expect(receivedMessage, equals(message)); - }); - - test('protocol with header and payload', () { - final writer = BinaryWriter() - // Header - ..writeUint8(1) // version - ..writeUint8(42) // message type - ..writeUint32(123456) // message id - // Payload - ..writeVarString('user@example.com') - ..writeVarUint(1000) - ..writeBool(true); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - // Read header - expect(reader.readUint8(), equals(1)); - expect(reader.readUint8(), equals(42)); - expect(reader.readUint32(), equals(123456)); - - // Read payload - expect(reader.readVarString(), equals('user@example.com')); - expect(reader.readVarUint(), equals(1000)); - expect(reader.readBool(), isTrue); - }); - - test('array of structures with VarInt lengths', () { - final writer = BinaryWriter(); - final items = [ - {'name': 'Item1', 'value': 100}, - {'name': 'Item2', 'value': 200}, - {'name': 'Item3', 'value': 300}, - ]; - - writer.writeVarUint(items.length); - for (final item in items) { - writer - ..writeVarString(item['name']! as String) - ..writeVarUint(item['value']! as int); - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - final count = reader.readVarUint(); - expect(count, equals(3)); - - for (var i = 0; i < count; i++) { - expect(reader.readVarString(), equals(items[i]['name'])); - expect(reader.readVarUint(), equals(items[i]['value'])); - } - }); - - test('conditional reading with hasBytes', () { - final writer = BinaryWriter() - ..writeUint32(42) - ..writeUint16(100); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint32(), equals(42)); - - // Check if there's data for another uint32, but only uint16 available - if (reader.hasBytes(4)) { - fail('Should not have 4 bytes'); - } else if (reader.hasBytes(2)) { - expect(reader.readUint16(), equals(100)); - } - }); - }); - }); -} diff --git a/test/integration/robustness_integration_test.dart b/test/integration/robustness_integration_test.dart new file mode 100644 index 0000000..d7f6f80 --- /dev/null +++ b/test/integration/robustness_integration_test.dart @@ -0,0 +1,49 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('Robustness Integration Tests', () { + test('Round-trip validation for all types', () { + final writer = BinaryWriter() + ..writeUint8(255) + ..writeInt16(-32768) + ..writeUint32(4294967295) + ..writeFloat64(3.14159) + ..writeVarString('Round-trip') + ..writeBool(true); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint8(), equals(255)); + expect(reader.readInt16(), equals(-32768)); + expect(reader.readUint32(), equals(4294967295)); + expect(reader.readFloat64(), equals(3.14159)); + expect(reader.readVarString(), equals('Round-trip')); + expect(reader.readBool(), isTrue); + }); + + test('Stress test - many small operations', () { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + + final reader = BinaryReader(writer.takeBytes()); + for (var i = 0; i < 1000; i++) { + expect(reader.readUint8(), equals(i % 256)); + } + }); + + test('Boundary conditions - writing exactly to buffer boundary', () { + final writer = BinaryWriter(initialBufferSize: 8) + // + // ignore: avoid_js_rounded_ints + ..writeUint64(0x1122334455667788); // Exactly 8 bytes + + expect(writer.bytesWritten, equals(8)); + final reader = BinaryReader(writer.takeBytes()); + // + // ignore: avoid_js_rounded_ints + expect(reader.readUint64(), equals(0x1122334455667788)); + }); + }); +} diff --git a/test/integration/types_integration_test.dart b/test/integration/types_integration_test.dart new file mode 100644 index 0000000..2705c71 --- /dev/null +++ b/test/integration/types_integration_test.dart @@ -0,0 +1,72 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('Integrated Types Tests', () { + group('String handling', () { + test('write and read mixed Unicode string', () { + final writer = BinaryWriter(); + const str = 'ASCII_Юникод_中文_🌍'; + writer.writeString(str); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readString(reader.availableBytes), equals(str)); + }); + }); + + group('Float special values', () { + test('write and read special floats', () { + final writer = BinaryWriter() + ..writeFloat32(double.nan) + ..writeFloat64(double.infinity); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readFloat32().isNaN, isTrue); + expect(reader.readFloat64(), equals(double.infinity)); + }); + }); + + group('Variable-length types', () { + test('write and read VarUint and VarInt', () { + final writer = BinaryWriter() + ..writeVarUint(300) + ..writeVarInt(-100); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarUint(), equals(300)); + expect(reader.readVarInt(), equals(-100)); + }); + + test('write and read VarBytes and VarString', () { + final writer = BinaryWriter() + ..writeVarBytes([1, 2, 3]) + ..writeVarString('Hello'); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarBytes(), equals([1, 2, 3])); + expect(reader.readVarString(), equals('Hello')); + }); + }); + + group('Boolean operations', () { + test('write and read multiple booleans', () { + final writer = BinaryWriter() + ..writeBool(true) + ..writeBool(false) + ..writeBool(true); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + }); + }); + + group('Large data cycles', () { + test('write and read large data set', () { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(1000, (i) => i % 256)); + writer.writeBytes(data); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readBytes(1000), equals(data)); + }); + }); + }); +} diff --git a/test/unit/binary_reader_basic_test.dart b/test/unit/binary_reader_basic_test.dart new file mode 100644 index 0000000..8bfb88b --- /dev/null +++ b/test/unit/binary_reader_basic_test.dart @@ -0,0 +1,93 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Basic Operations', () { + test('reads Uint8 correctly', () { + final buffer = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer); + expect(reader.readUint8(), equals(1)); + expect(reader.availableBytes, equals(0)); + }); + + test('reads Int8 correctly', () { + final buffer = Uint8List.fromList([0xFF]); + final reader = BinaryReader(buffer); + expect(reader.readInt8(), equals(-1)); + expect(reader.availableBytes, equals(0)); + }); + + test('reads Uint16 in big-endian', () { + final buffer = Uint8List.fromList([0x01, 0x00]); + final reader = BinaryReader(buffer); + expect(reader.readUint16(), equals(256)); + }); + + test('reads Uint16 in little-endian', () { + final buffer = Uint8List.fromList([0x00, 0x01]); + final reader = BinaryReader(buffer); + expect(reader.readUint16(.little), equals(256)); + }); + + test('reads Uint32 correctly', () { + final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); + final reader = BinaryReader(buffer); + expect(reader.readUint32(), equals(65536)); + }); + + test('reads Uint64 correctly', () { + final buffer = Uint8List.fromList([0, 0, 0, 1, 0, 0, 0, 0]); + final reader = BinaryReader(buffer); + expect(reader.readUint64(), equals(4294967296)); + }); + + test('reads Float32 correctly', () { + final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); + final reader = BinaryReader(buffer); + expect(reader.readFloat32(), closeTo(3.1415927, 0.0000001)); + }); + + test('reads Float64 correctly', () { + final buffer = Uint8List.fromList([ + 0x40, + 0x09, + 0x21, + 0xFB, + 0x54, + 0x44, + 0x2D, + 0x18, + ]); + final reader = BinaryReader(buffer); + expect( + reader.readFloat64(), + closeTo(3.141592653589793, 0.000000000000001), + ); + }); + + test('readBool correctly', () { + final buffer = Uint8List.fromList([0x01, 0x00]); + final reader = BinaryReader(buffer); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + }); + + test('availableBytes returns correct number', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); + expect(reader.availableBytes, equals(3)); + reader.readUint8(); + expect(reader.availableBytes, equals(2)); + }); + + test('offset returns current position', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); + expect(reader.offset, equals(0)); + reader.readUint8(); + expect(reader.offset, equals(1)); + }); + }); +} diff --git a/test/unit/binary_reader_buffer_test.dart b/test/unit/binary_reader_buffer_test.dart new file mode 100644 index 0000000..351fd82 --- /dev/null +++ b/test/unit/binary_reader_buffer_test.dart @@ -0,0 +1,73 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Buffer Operations', () { + test('peekBytes returns bytes without changing offset', () { + final buffer = Uint8List.fromList([0x10, 0x20, 0x30, 0x40]); + final reader = BinaryReader(buffer); + + final peeked = reader.peekBytes(2); + expect(peeked, equals([0x10, 0x20])); + expect(reader.offset, equals(0)); + + expect(reader.readUint8(), equals(0x10)); + }); + + test('readBytes returns view of original buffer', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + final bytes = reader.readBytes(3); + expect(bytes, isA()); + expect(bytes, equals([1, 2, 3])); + }); + + test('Buffer sharing - multiple readers on same buffer', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader1 = BinaryReader(buffer); + final reader2 = BinaryReader(buffer); + + expect(reader1.readUint8(), equals(1)); + expect(reader2.readUint8(), equals(1)); + expect(reader1.readUint8(), equals(2)); + }); + + group('rebind', () { + test('rebind replaces buffer and resets offset to 0', () { + final buffer1 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader(buffer1)..readUint8(); + expect(reader.offset, equals(1)); + + final buffer2 = Uint8List.fromList([0x10, 0x20, 0x30]); + reader.rebind(buffer2); + + expect(reader.offset, equals(0)); + expect(reader.length, equals(3)); + expect(reader.readUint8(), equals(0x10)); + }); + + test('rebind with zero-length buffer', () { + final buffer1 = Uint8List.fromList([0x01, 0x02]); + final reader = BinaryReader(buffer1) + ..readUint8() + ..rebind(Uint8List(0)); + expect(reader.offset, equals(0)); + expect(reader.length, equals(0)); + expect(reader.readUint8, throwsA(isA())); + }); + }); + + test('Mixed endianness operations', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x02, 0x01]); + final reader = BinaryReader(buffer); + + expect(reader.readUint16(), equals(0x0102)); // Big + expect( + reader.readUint16(.little), + equals(0x0102), + ); // Little (reversed 0x0201 is 0x0102) + }); + }); +} diff --git a/test/unit/binary_reader_edge_cases_test.dart b/test/unit/binary_reader_edge_cases_test.dart new file mode 100644 index 0000000..ccbcc23 --- /dev/null +++ b/test/unit/binary_reader_edge_cases_test.dart @@ -0,0 +1,78 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Edge Cases', () { + test('read beyond buffer throws RangeError', () { + final buffer = Uint8List.fromList([0x01, 0x02]); + final reader = BinaryReader(buffer); + expect(reader.readUint32, throwsA(isA())); + }); + + test('negative length input throws RangeError', () { + final buffer = Uint8List.fromList([0x01, 0x02]); + final reader = BinaryReader(buffer); + expect(() => reader.readBytes(-1), throwsA(isA())); + expect(() => reader.skip(-1), throwsA(isA())); + }); + + group('Partial read scenarios', () { + test('interleaved VarInt and fixed-size reads', () { + final writer = BinaryWriter() + ..writeVarUint(127) + ..writeUint8(42) + ..writeVarInt(-1) + ..writeUint16(1000); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarUint(), equals(127)); + expect(reader.readUint8(), equals(42)); + expect(reader.readVarInt(), equals(-1)); + expect(reader.readUint16(), equals(1000)); + }); + }); + + group('Concise API', () { + test('operator [] returns byte at absolute index', () { + final buffer = Uint8List.fromList([10, 20, 30, 40]); + final reader = BinaryReader(buffer); + expect(reader[0], equals(10)); + expect(reader[1], equals(20)); + expect(reader.offset, equals(0)); + }); + + test('call() is an alias for readBytes', () { + final buffer = Uint8List.fromList([10, 20, 30, 40]); + final reader = BinaryReader(buffer); + expect(reader.call(2), equals([10, 20])); + expect(reader.offset, equals(2)); + }); + }); + + group('fromList', () { + test('fromList creates reader from List', () { + final bytes = [0x01, 0x02]; + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('fromList copies data', () { + final bytes = [0x01, 0x02]; + final reader = BinaryReader.fromList(bytes); + bytes[0] = 0xFF; + expect(reader.readUint8(), equals(1)); + }); + }); + + group('Coverage edge cases', () { + test('peekByte returns byte without advancing', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.offset, equals(0)); + }); + }); + }); +} diff --git a/test/unit/binary_reader_navigation_test.dart b/test/unit/binary_reader_navigation_test.dart new file mode 100644 index 0000000..64a9139 --- /dev/null +++ b/test/unit/binary_reader_navigation_test.dart @@ -0,0 +1,67 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Navigation', () { + test('skip method correctly updates the offset', () { + final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader(buffer)..skip(2); + expect(reader.offset, equals(2)); + expect(reader.readUint8(), equals(0x02)); + }); + + test('seek method sets position correctly', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(2); + expect(reader.offset, equals(2)); + expect(reader.readUint8(), equals(3)); + + reader.seek(0); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('rewind method moves back correctly', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(3) // offset 3 + ..rewind(2); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('hasBytes returns true when enough bytes available', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + expect(reader.hasBytes(1), isTrue); + expect(reader.hasBytes(5), isTrue); + expect(reader.hasBytes(6), isFalse); + }); + + group('baseOffset handling', () { + test('readBytes works correctly with non-zero baseOffset', () { + final largeBuffer = Uint8List.fromList(List.generate(100, (i) => i)); + final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); + final reader = BinaryReader(subBuffer); + + final bytes = reader.readBytes(5); + expect(bytes, equals([50, 51, 52, 53, 54])); + expect(reader.availableBytes, equals(5)); + }); + }); + + test('throws on seeking beyond buffer', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); + expect(() => reader.seek(4), throwsA(isA())); + }); + + test('throws when rewinding beyond start', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer)..readUint8(); // offset 1 + expect(() => reader.rewind(2), throwsA(isA())); + }); + }); +} diff --git a/test/unit/binary_reader_string_test.dart b/test/unit/binary_reader_string_test.dart new file mode 100644 index 0000000..245b0df --- /dev/null +++ b/test/unit/binary_reader_string_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader String Operations', () { + test('readString correctly', () { + const str = 'Hello, world!'; + final encoded = utf8.encode(str); + final reader = BinaryReader(Uint8List.fromList(encoded)); + expect(reader.readString(encoded.length), equals(str)); + }); + + test('readString with multi-byte UTF-8 characters', () { + const str = 'Привет, мир!'; + final encoded = utf8.encode(str); + final reader = BinaryReader(Uint8List.fromList(encoded)); + expect(reader.readString(encoded.length), equals(str)); + }); + + group('Malformed UTF-8', () { + test('readString with allowMalformed=true handles invalid UTF-8', () { + final buffer = Uint8List.fromList([ + 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" + 0xFF, // Invalid byte + 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" + ]); + final reader = BinaryReader(buffer); + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, contains('Hello')); + expect(result, contains('World')); + }); + + test('readString with allowMalformed=false throws on invalid UTF-8', () { + final buffer = Uint8List.fromList([0xFF, 0xFE]); + final reader = BinaryReader(buffer); + expect(() => reader.readString(2), throwsA(isA())); + }); + }); + + group('Lone surrogate pairs', () { + test('readString handles lone high surrogate', () { + // Surrogate in isolation is malformed UTF-8 when encoded/decoded + final buffer = Uint8List.fromList([0xED, 0xA0, 0x80]); // U+D800 + final reader = BinaryReader(buffer); + final result = reader.readString(buffer.length, allowMalformed: true); + expect(result, isNotEmpty); + }); + }); + }); +} diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart deleted file mode 100644 index 898e6ec..0000000 --- a/test/unit/binary_reader_test.dart +++ /dev/null @@ -1,2194 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -void main() { - group('BinaryReader', () { - test('reads Uint8 correctly', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8(), equals(1)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int8 correctly', () { - final buffer = Uint8List.fromList([0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt8(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint16 in big-endian', () { - final buffer = Uint8List.fromList([0x01, 0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(256)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint16 in little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(.little), equals(256)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int16 in big-endian', () { - final buffer = Uint8List.fromList([0xFF, 0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt16(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int16 in little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x80]); - final reader = BinaryReader(buffer); - - expect(reader.readInt16(.little), equals(-32768)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint32 in big-endian', () { - final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32(), equals(65536)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint32 in little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x00, 0x01, 0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32(.little), equals(65536)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int32 in big-endian', () { - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt32(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int32 in little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x00, 0x00, 0x80]); - final reader = BinaryReader(buffer); - - expect(reader.readInt32(.little), equals(-2147483648)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint64 in big-endian', () { - final buffer = Uint8List.fromList([ - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x00, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readUint64(), equals(4294967296)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Uint64 in little-endian', () { - final buffer = Uint8List.fromList([ - 0x00, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readUint64(.little), equals(4294967296)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int64 in big-endian', () { - final buffer = Uint8List.fromList([ - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readInt64(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Int64 in little-endian', () { - final buffer = Uint8List.fromList([ - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x80, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readInt64(.little), equals(-9223372036854775808)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Float32 in big-endian', () { - final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); // 3.1415927 - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), closeTo(3.1415927, 0.0000001)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Float32 in little-endian', () { - final buffer = Uint8List.fromList([0xDB, 0x0F, 0x49, 0x40]); // 3.1415927 - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(.little), closeTo(3.1415927, 0.0000001)); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Float64 in big-endian', () { - final buffer = Uint8List.fromList([ - 0x40, - 0x09, - 0x21, - 0xFB, - 0x54, - 0x44, - 0x2D, - 0x18, - ]); // 3.141592653589793 - final reader = BinaryReader(buffer); - - expect( - reader.readFloat64(), - closeTo(3.141592653589793, 0.000000000000001), - ); - expect(reader.availableBytes, equals(0)); - }); - - test('reads Float64 in little-endian', () { - final buffer = Uint8List.fromList([ - 0x18, - 0x2D, - 0x44, - 0x54, - 0xFB, - 0x21, - 0x09, - 0x40, - ]); // 3.141592653589793 - final reader = BinaryReader(buffer); - - expect( - reader.readFloat64(.little), - closeTo(3.141592653589793, 0.000000000000001), - ); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt single byte (0)', () { - final buffer = Uint8List.fromList([0]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(0)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt single byte (127)', () { - final buffer = Uint8List.fromList([127]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(127)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt two bytes (128)', () { - final buffer = Uint8List.fromList([0x80, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(128)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt two bytes (300)', () { - final buffer = Uint8List.fromList([0xAC, 0x02]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(300)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt three bytes (16384)', () { - final buffer = Uint8List.fromList([0x80, 0x80, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(16384)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt four bytes (2097151)', () { - final buffer = Uint8List.fromList([0xFF, 0xFF, 0x7F]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(2097151)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt five bytes (268435455)', () { - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0x7F]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(268435455)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarInt large value', () { - final buffer = Uint8List.fromList([0x80, 0x80, 0x80, 0x80, 0x04]); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(1 << 30)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for zero', () { - final buffer = Uint8List.fromList([0]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(0)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for positive value 1', () { - final buffer = Uint8List.fromList([2]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(1)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for negative value -1', () { - final buffer = Uint8List.fromList([1]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for positive value 2', () { - final buffer = Uint8List.fromList([4]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(2)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for negative value -2', () { - final buffer = Uint8List.fromList([3]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(-2)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for large positive value', () { - final buffer = Uint8List.fromList([0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(2147483647)); - expect(reader.availableBytes, equals(0)); - }); - - test('readZigZag encoding for large negative value', () { - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(-2147483648)); - expect(reader.availableBytes, equals(0)); - }); - - test('readVarUint throws on truncated varint', () { - // VarInt with continuation bit set but no following byte - final buffer = Uint8List.fromList([0x80]); // MSB=1, expects more bytes - final reader = BinaryReader(buffer); - - expect(reader.readVarUint, throwsA(isA())); - }); - - test('readVarUint throws on incomplete multi-byte varint', () { - // Two-byte VarInt with only first byte - final buffer = Uint8List.fromList([0xFF]); // All continuation bits set - final reader = BinaryReader(buffer); - - expect(reader.readVarUint, throwsA(isA())); - }); - - test('readVarUint throws FormatException on too long varint', () { - // 11 bytes with all continuation bits set (exceeds 10-byte limit) - final buffer = Uint8List.fromList([ - 0x80, 0x80, 0x80, 0x80, 0x80, // - 0x80, 0x80, 0x80, 0x80, 0x80, // - 0x80, // 11th byte - ]); - final reader = BinaryReader(buffer); - - expect( - reader.readVarUint, - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('VarInt is too long'), - ), - ), - ); - }); - - test('readVarInt throws on truncated zigzag', () { - // Truncated VarInt (continuation bit set but no next byte) - final buffer = Uint8List.fromList([0x80]); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt, throwsA(isA())); - }); - - test('readBytes', () { - final data = [0x01, 0x02, 0x03, 0x04, 0x05]; - final buffer = Uint8List.fromList(data); - final reader = BinaryReader(buffer); - - expect(reader.readBytes(5), equals(data)); - expect(reader.availableBytes, equals(0)); - }); - - test('readString', () { - const str = 'Hello, world!'; - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - expect(reader.availableBytes, equals(0)); - }); - - test('readString with multi-byte UTF-8 characters', () { - const str = 'Привет, мир!'; // "Hello, world!" in Russian - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - expect(reader.availableBytes, equals(0)); - }); - - test('availableBytes returns correct number of remaining bytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); - - expect(reader.availableBytes, equals(4)); - reader.readUint8(); - expect(reader.availableBytes, equals(3)); - reader.readBytes(2); - expect(reader.availableBytes, equals(1)); - }); - - test( - 'peekBytes returns correct bytes without changing the internal state', - () { - final buffer = Uint8List.fromList([0x10, 0x20, 0x30, 0x40, 0x50]); - final reader = BinaryReader(buffer); - - final peekedBytes = reader.peekBytes(3); - expect(peekedBytes, equals([0x10, 0x20, 0x30])); - expect(reader.offset, equals(0)); - reader.readUint8(); // Now usedBytes should be 1 - final peekedBytesWithOffset = reader.peekBytes(2, 2); - expect(peekedBytesWithOffset, equals([0x30, 0x40])); - expect(reader.offset, equals(1)); - }, - ); - - test('skip method correctly updates the offset', () { - final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer)..skip(2); - expect(reader.offset, equals(2)); - expect(reader.readUint8(), equals(0x02)); - }); - - test('read beyond buffer throws RangeError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32, throwsA(isA())); - }); - - test('negative length input throws RangeError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(() => reader.readBytes(-1), throwsA(isA())); - expect(() => reader.skip(-5), throwsA(isA())); - expect(() => reader.peekBytes(-2), throwsA(isA())); - }); - - test('reading from empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8, throwsA(isA())); - }); - - test('reading with offset at end of buffer', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer)..skip(2); - - expect(reader.readUint8, throwsA(isA())); - }); - - test('peekBytes beyond buffer throws RangeError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(() => reader.peekBytes(3), throwsA(isA())); - expect(() => reader.peekBytes(1, 2), throwsA(isA())); - }); - - test('readString with insufficient bytes throws RangeError', () { - final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' - final reader = BinaryReader(buffer); - - expect(() => reader.readString(5), throwsA(isA())); - }); - - test('readBytes with insufficient bytes throws RangeError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(() => reader.readBytes(3), throwsA(isA())); - }); - - test('read methods throw RangeError when not enough bytes', () { - final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32, throwsA(isA())); - expect(reader.readInt32, throwsA(isA())); - expect(reader.readFloat32, throwsA(isA())); - }); - - test( - 'readUint64 and readInt64 with insufficient bytes throw RangeError', - () { - final buffer = Uint8List.fromList(List.filled(7, 0x00)); // Only 7 bytes - final reader = BinaryReader(buffer); - - expect(reader.readUint64, throwsA(isA())); - expect(reader.readInt64, throwsA(isA())); - }, - ); - - test('skip beyond buffer throws RangeError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(() => reader.skip(3), throwsA(isA())); - }); - - test('read and verify multiple values sequentially', () { - final buffer = Uint8List.fromList([ - 0x01, // Uint8 - 0xFF, // Int8 - 0x00, 0x01, // Uint16 big-endian - 0xFF, 0xFF, // Int16 big-endian - 0x00, 0x00, 0x00, 0x01, // Uint32 big-endian - 0xFF, 0xFF, 0xFF, 0xFF, // Int32 big-endian - 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Float64 (double 2.0) - ]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8(), equals(0x01)); - expect(reader.readInt8(), equals(-1)); - expect(reader.readUint16(), equals(1)); - expect(reader.readInt16(), equals(-1)); - expect(reader.readUint32(), equals(1)); - expect(reader.readInt32(), equals(-1)); - expect(reader.readFloat64(), equals(2.0)); - }); - - group('Boundary checks', () { - test('readUint8 throws when buffer is empty', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8, throwsA(isA())); - }); - - test('readInt8 throws when buffer is empty', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readInt8, throwsA(isA())); - }); - - test('readUint16 throws when only 1 byte available', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16, throwsA(isA())); - }); - - test('readInt16 throws when only 1 byte available', () { - final buffer = Uint8List.fromList([0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt16, throwsA(isA())); - }); - - test('readUint32 throws when only 3 bytes available', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32, throwsA(isA())); - }); - - test('readInt32 throws when only 3 bytes available', () { - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt32, throwsA(isA())); - }); - - test('readUint64 throws when only 7 bytes available', () { - final buffer = Uint8List.fromList([ - 0x01, - 0x02, - 0x03, - 0x04, - 0x05, - 0x06, - 0x07, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readUint64, throwsA(isA())); - }); - - test('readInt64 throws when only 7 bytes available', () { - final buffer = Uint8List.fromList([ - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readInt64, throwsA(isA())); - }); - - test('readFloat32 throws when only 3 bytes available', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32, throwsA(isA())); - }); - - test('readFloat64 throws when only 7 bytes available', () { - final buffer = Uint8List.fromList([ - 0x01, - 0x02, - 0x03, - 0x04, - 0x05, - 0x06, - 0x07, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readFloat64, throwsA(isA())); - }); - - test('readBytes throws when requested length exceeds available', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(() => reader.readBytes(5), throwsA(isA())); - }); - - test('readBytes throws when length is negative', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(() => reader.readBytes(-1), throwsA(isA())); - }); - - test('readString throws when requested length exceeds available', () { - final buffer = Uint8List.fromList([0x48, 0x65, 0x6C]); // "Hel" - final reader = BinaryReader(buffer); - - expect(() => reader.readString(10), throwsA(isA())); - }); - - test('multiple reads exceed buffer size', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer) - ..readUint8() // 1 byte read, 3 remaining - ..readUint8() // 1 byte read, 2 remaining - ..readUint16(); // 2 bytes read, 0 remaining - - expect(reader.readUint8, throwsA(isA())); - }); - - test('peekBytes throws when length is negative', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(() => reader.peekBytes(-1), throwsA(isA())); - }); - - test('skip throws when length exceeds available bytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(() => reader.skip(5), throwsA(isA())); - }); - - test('skip throws when length is negative', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(() => reader.skip(-1), throwsA(isA())); - }); - }); - - group('offset getter', () { - test('offset returns current reading position', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); - - expect(reader.offset, equals(0)); - - reader.readUint8(); - expect(reader.offset, equals(1)); - - reader.readUint16(); - expect(reader.offset, equals(3)); - - reader.readUint8(); - expect(reader.offset, equals(4)); - }); - - group('Special values and edge cases', () { - test('readString with empty UTF-8 string', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readString(0), equals('')); - expect(reader.availableBytes, equals(0)); - }); - - test('readString with emoji characters', () { - const str = '🚀👨‍👩‍👧‍👦'; // Rocket and family emoji - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - expect(reader.availableBytes, equals(0)); - }); - - test('readFloat32 with NaN', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .nan); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('readFloat32 with Infinity', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .infinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), equals(double.infinity)); - }); - - test('readFloat32 with negative Infinity', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, .negativeInfinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), equals(double.negativeInfinity)); - }); - - test('readFloat64 with NaN', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .nan); - final reader = BinaryReader(buffer); - - expect(reader.readFloat64().isNaN, isTrue); - }); - - test('readFloat64 with Infinity', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .infinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat64(), equals(double.infinity)); - }); - - test('readFloat64 with negative Infinity', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, .negativeInfinity); - final reader = BinaryReader(buffer); - - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }); - - test('readFloat64 with negative zero', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, -0); - final reader = BinaryReader(buffer); - - final value = reader.readFloat64(); - expect(value, equals(0.0)); - expect(value.isNegative, isTrue); - }); - - test('readUint64 with maximum value', () { - final buffer = Uint8List.fromList([ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // - ]); - final reader = BinaryReader(buffer); - - // Max Uint64 is 2^64 - 1 = 18446744073709551615 - // In Dart, this wraps to -1 for signed int representation - expect(reader.readUint64(), equals(0xFFFFFFFFFFFFFFFF)); - }); - - test('peekBytes with zero length', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); - - expect(reader.peekBytes(0), equals([])); - expect(reader.offset, equals(0)); - }); - - test('peekBytes with explicit zero offset', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); - - final peeked = reader.peekBytes(2, 0); - expect(peeked, equals([0x01, 0x02])); - expect(reader.offset, equals(1)); - }); - }); - - group('Malformed UTF-8', () { - test('readString with allowMalformed=true handles invalid UTF-8', () { - // Invalid UTF-8 sequence: 0xFF is not valid in UTF-8 - final buffer = Uint8List.fromList([ - 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" - 0xFF, // Invalid byte - 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" - ]); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, contains('Hello')); - expect(result, contains('World')); - }); - - test( - 'readString with allowMalformed=false throws on invalid UTF-8', - () { - final buffer = Uint8List.fromList([0xFF, 0xFE, 0xFD]); - final reader = BinaryReader(buffer); - - expect( - () => reader.readString(buffer.length), - throwsA(isA()), - ); - }, - ); - - test('readString handles truncated multi-byte sequence', () { - final buffer = Uint8List.fromList([0xE0, 0xA0]); - final reader = BinaryReader(buffer); - - expect( - () => reader.readString(buffer.length), - throwsA(isA()), - ); - }); - - test('readString with allowMalformed handles truncated sequence', () { - final buffer = Uint8List.fromList([ - 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" - 0xE0, 0xA0, // Incomplete 3-byte sequence - ]); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, startsWith('Hello')); - }); - }); - - group('Lone surrogate pairs', () { - test('readString handles lone high surrogate', () { - final buffer = utf8.encode('Test\uD800End'); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, isNotEmpty); - }); - - test('readString handles lone low surrogate', () { - final buffer = utf8.encode('Test\uDC00End'); - final reader = BinaryReader(buffer); - - final result = reader.readString(buffer.length, allowMalformed: true); - expect(result, isNotEmpty); - }); - }); - - group('peekBytes advanced', () { - test( - 'peekBytes with offset beyond current position but within buffer', - () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - final reader = BinaryReader(buffer) - ..readUint8() - ..readUint8(); - - final peeked = reader.peekBytes(3, 5); - expect(peeked, equals([6, 7, 8])); - expect(reader.offset, equals(2)); - }, - ); - - test('peekBytes at buffer boundary', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - final peeked = reader.peekBytes(2, 3); - expect(peeked, equals([4, 5])); - expect(reader.offset, equals(0)); - }); - - test('peekBytes exactly at end with zero length', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer); - - final peeked = reader.peekBytes(0, 3); - expect(peeked, isEmpty); - expect(reader.offset, equals(0)); - }); - }); - - group('Sequential operations', () { - test('alternating read and peek operations', () { - final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8(), equals(10)); - expect(reader.peekBytes(2), equals([20, 30])); - expect(reader.readUint8(), equals(20)); - expect(reader.peekBytes(1, 3), equals([40])); - expect(reader.readUint8(), equals(30)); - }); - }); - - group('Large buffer operations', () { - test('readBytes with very large length', () { - const largeSize = 1000000; - final buffer = Uint8List(largeSize); - for (var i = 0; i < largeSize; i++) { - buffer[i] = i % 256; - } - - final reader = BinaryReader(buffer); - final result = reader.readBytes(largeSize); - - expect(result.length, equals(largeSize)); - expect(reader.availableBytes, equals(0)); - }); - - test('skip large amount of data', () { - final buffer = Uint8List(100000); - final reader = BinaryReader(buffer)..skip(50000); - expect(reader.offset, equals(50000)); - expect(reader.availableBytes, equals(50000)); - }); - }); - - group('Buffer sharing', () { - test('multiple readers can read same buffer concurrently', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader1 = BinaryReader(buffer); - final reader2 = BinaryReader(buffer); - - expect(reader1.readUint8(), equals(1)); - expect(reader2.readUint8(), equals(1)); - expect(reader1.readUint8(), equals(2)); - expect(reader2.readUint16(), equals(0x0203)); - }); - - test('peekBytes returns independent views', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - final peek1 = reader.peekBytes(3); - final peek2 = reader.peekBytes(3); - - expect(peek1, equals([1, 2, 3])); - expect(peek2, equals([1, 2, 3])); - expect(identical(peek1, peek2), isFalse); - }); - }); - - group('Zero-copy verification', () { - test('readBytes returns view of original buffer', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - final bytes = reader.readBytes(3); - - expect(bytes, isA()); - expect(bytes.length, equals(3)); - }); - - test('peekBytes returns view of original buffer', () { - final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); - - final peeked = reader.peekBytes(3); - - expect(peeked, isA()); - expect(peeked, equals([10, 20, 30])); - }); - }); - - group('Mixed endianness operations', () { - test('reading alternating big and little endian values', () { - final writer = BinaryWriter() - ..writeUint16(0x1234) - ..writeUint16(0x5678, .little) - ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, .little); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(.little), equals(0x5678)); - expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(.little), equals(0x11223344)); - }); - - test('float values with different endianness', () { - final writer = BinaryWriter() - ..writeFloat32(3.14) - ..writeFloat32(2.71, .little) - ..writeFloat64(1.414) - ..writeFloat64(1.732, .little); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); - expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); - }); - }); - - group('Boundary conditions at exact sizes', () { - test('buffer exactly matches read size', () { - final buffer = Uint8List.fromList([1, 2, 3, 4]); - final reader = BinaryReader(buffer); - - final result = reader.readBytes(4); - expect(result, equals([1, 2, 3, 4])); - expect(reader.availableBytes, equals(0)); - }); - - test('reading exactly to boundary multiple times', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(0x0102)); - expect(reader.readUint16(), equals(0x0304)); - expect(reader.readUint16(), equals(0x0506)); - expect(reader.availableBytes, equals(0)); - }); - }); - - group('baseOffset handling', () { - test('readBytes works correctly with non-zero baseOffset', () { - // Create a larger buffer and take a sublist - // (which will have non-zero baseOffset) - final largeBuffer = Uint8List(100); - for (var i = 0; i < 100; i++) { - largeBuffer[i] = i; - } - - // Create a view starting at offset 50 - final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); - final reader = BinaryReader(subBuffer); - - // Read bytes and verify they match the expected values (50-59) - final bytes = reader.readBytes(5); - expect(bytes, equals([50, 51, 52, 53, 54])); - expect(reader.availableBytes, equals(5)); - }); - - test('readString works correctly with non-zero baseOffset', () { - // Create a buffer with text data - const text = 'Hello, World!'; - final encoded = utf8.encode(text); - - // Create a larger buffer and copy the text at an offset - final largeBuffer = Uint8List(100) - ..setRange(30, 30 + encoded.length, encoded); - - // Create a view of just the text portion - final subBuffer = Uint8List.sublistView( - largeBuffer, - 30, - 30 + encoded.length, - ); - final reader = BinaryReader(subBuffer); - - final result = reader.readString(encoded.length); - expect(result, equals(text)); - expect(reader.availableBytes, equals(0)); - }); - - test('peekBytes works correctly with non-zero baseOffset', () { - final largeBuffer = Uint8List(50); - for (var i = 0; i < 50; i++) { - largeBuffer[i] = i; - } - - // Create a view starting at offset 20 - final subBuffer = Uint8List.sublistView(largeBuffer, 20, 30); - final reader = BinaryReader(subBuffer); - - // Peek at bytes without consuming them - final peeked = reader.peekBytes(5); - expect(peeked, equals([20, 21, 22, 23, 24])); - expect(reader.offset, equals(0)); - - // Now read and verify - final read = reader.readBytes(5); - expect(read, equals([20, 21, 22, 23, 24])); - expect(reader.offset, equals(5)); - }); - - test('readUint16/32/64 work correctly with non-zero baseOffset', () { - final largeBuffer = Uint8List(100); - - // Write some values at offset 40 - final writer = BinaryWriter() - ..writeUint16(0x1234) - ..writeUint32(0x56789ABC) - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - ..writeUint64(0x0FEDCBA987654321); - - final data = writer.takeBytes(); - largeBuffer.setRange(40, 40 + data.length, data); - - // Create a view starting at offset 40 - final subBuffer = Uint8List.sublistView( - largeBuffer, - 40, - 40 + data.length, - ); - final reader = BinaryReader(subBuffer); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint32(), equals(0x56789ABC)); - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - expect(reader.readUint64(), equals(0x0FEDCBA987654321)); - expect(reader.availableBytes, equals(0)); - }); - - test('multiple readers from different offsets', () { - final largeBuffer = Uint8List(100); - for (var i = 0; i < 100; i++) { - largeBuffer[i] = i; - } - - // Create two readers from different offsets - final reader1 = BinaryReader( - Uint8List.sublistView(largeBuffer, 10, 20), - ); - final reader2 = BinaryReader( - Uint8List.sublistView(largeBuffer, 50, 60), - ); - - expect(reader1.readUint8(), equals(10)); - expect(reader2.readUint8(), equals(50)); - - expect(reader1.readBytes(3), equals([11, 12, 13])); - expect(reader2.readBytes(3), equals([51, 52, 53])); - }); - - test('readVarBytes basic usage', () { - final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([1, 2, 3, 4])); - }); - - test('readVarBytes with empty array', () { - final writer = BinaryWriter()..writeVarBytes([]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([])); - }); - - test('readVarBytes multiple arrays', () { - final writer = BinaryWriter() - ..writeVarBytes([10, 20]) - ..writeVarBytes([30, 40, 50]) - ..writeVarBytes([60]); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarBytes(), equals([10, 20])); - expect(reader.readVarBytes(), equals([30, 40, 50])); - expect(reader.readVarBytes(), equals([60])); - }); - - test('readVarBytes with large array', () { - final writer = BinaryWriter(); - final data = List.generate(500, (i) => (i * 3) & 0xFF); - writer.writeVarBytes(data); - final reader = BinaryReader(writer.takeBytes()); - - final result = reader.readVarBytes(); - expect(result, equals(data)); - expect(result.length, equals(500)); - }); - - test('readVarBytes throws on truncated length', () { - final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint - final reader = BinaryReader(bytes); - - expect( - reader.readVarBytes, - throwsA(isA()), - ); - }); - - test('readVarBytes throws when not enough data', () { - final bytes = Uint8List.fromList([5, 1, 2]); // Length=5, only 2 bytes - final reader = BinaryReader(bytes); - - expect( - reader.readVarBytes, - throwsA(isA()), - ); - }); - - test('readVarBytes preserves binary data', () { - final writer = BinaryWriter(); - // Test with all byte values 0-255 - final allBytes = List.generate(256, (i) => i); - writer.writeVarBytes(allBytes); - - final reader = BinaryReader(writer.takeBytes()); - final result = reader.readVarBytes(); - - expect(result, equals(allBytes)); - for (var i = 0; i < 256; i++) { - expect(result[i], equals(i), reason: 'Byte $i mismatch'); - } - }); - - test('readVarString basic usage', () { - final writer = BinaryWriter()..writeVarString('Hello'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('Hello')); - }); - - test('readVarString with UTF-8 multi-byte', () { - final writer = BinaryWriter()..writeVarString('世界'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('世界')); - }); - - test('readVarString with emoji', () { - final writer = BinaryWriter()..writeVarString('🌍🎉'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('🌍🎉')); - }); - - test('readVarString with empty string', () { - final writer = BinaryWriter()..writeVarString(''); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('')); - }); - - test('readVarString multiple strings', () { - final writer = BinaryWriter() - ..writeVarString('First') - ..writeVarString('Second 测试') - ..writeVarString('Third 🎉'); - final reader = BinaryReader(writer.takeBytes()); - - expect(reader.readVarString(), equals('First')); - expect(reader.readVarString(), equals('Second 测试')); - expect(reader.readVarString(), equals('Third 🎉')); - }); - - test('readVarString with allowMalformed=false on valid data', () { - final writer = BinaryWriter()..writeVarString('Valid UTF-8'); - final reader = BinaryReader(writer.takeBytes()); - - expect( - reader.readVarString, - returnsNormally, - ); - }); - - test('readVarString throws on truncated length', () { - final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint - final reader = BinaryReader(bytes); - - expect( - reader.readVarString, - throwsA(isA()), - ); - }); - - test('readVarString throws when not enough data for string', () { - final bytes = Uint8List.fromList([ - 5, - 65, - 66, - ]); // Length=5, only 2 bytes - final reader = BinaryReader(bytes); - - expect( - reader.readVarString, - throwsA(isA()), - ); - }); - - test('baseOffset with readString containing multi-byte UTF-8', () { - const text = 'Привет мир! 🌍'; - final encoded = utf8.encode(text); - - final largeBuffer = Uint8List(200) - ..setRange(75, 75 + encoded.length, encoded); - - final subBuffer = Uint8List.sublistView( - largeBuffer, - 75, - 75 + encoded.length, - ); - final reader = BinaryReader(subBuffer); - - final result = reader.readString(encoded.length); - expect(result, equals(text)); - }); - }); - - group('Getter properties', () { - test('offset getter returns current read position', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeUint16(2) - ..writeUint32(3); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.offset, equals(0)); - reader.readUint8(); - expect(reader.offset, equals(1)); - reader.readUint16(); - expect(reader.offset, equals(3)); - reader.readUint32(); - expect(reader.offset, equals(7)); - }); - - test('length getter returns total buffer length', () { - final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(bytes); - - expect(reader.length, equals(5)); - reader.readUint8(); - expect(reader.length, equals(5)); // Length doesn't change - reader.readUint32(); - expect(reader.length, equals(5)); - }); - - test('offset and length used together to calculate availableBytes', () { - final bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(bytes); - - expect(reader.length, equals(8)); - expect(reader.offset, equals(0)); - expect(reader.availableBytes, equals(8)); - - reader.readUint32(); - expect(reader.offset, equals(4)); - expect(reader.length, equals(8)); - expect(reader.availableBytes, equals(4)); - - reader.readUint32(); - expect(reader.offset, equals(8)); - expect(reader.length, equals(8)); - expect(reader.availableBytes, equals(0)); - }); - }); - - group('readBool', () { - test('reads false when byte is 0', () { - final buffer = Uint8List.fromList([0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readBool(), isFalse); - expect(reader.availableBytes, equals(0)); - }); - - test('reads true when byte is 1', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readBool(), isTrue); - expect(reader.availableBytes, equals(0)); - }); - - test('reads true when byte is any non-zero value', () { - final testValues = [1, 42, 127, 128, 255]; - for (final value in testValues) { - final buffer = Uint8List.fromList([value]); - final reader = BinaryReader(buffer); - - expect( - reader.readBool(), - isTrue, - reason: 'Value $value should be true', - ); - } - }); - - test('reads multiple boolean values correctly', () { - final buffer = Uint8List.fromList([0x01, 0x00, 0xFF, 0x00, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - expect(reader.availableBytes, equals(0)); - }); - - test('advances offset correctly', () { - final buffer = Uint8List.fromList([0x01, 0x00, 0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.offset, equals(0)); - reader.readBool(); - expect(reader.offset, equals(1)); - reader.readBool(); - expect(reader.offset, equals(2)); - reader.readBool(); - expect(reader.offset, equals(3)); - }); - - test('throws when reading from empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readBool, throwsA(isA())); - }); - - test('throws when no bytes available', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer)..readBool(); // Consume the byte - expect(reader.readBool, throwsA(isA())); - }); - }); - - group('readRemainingBytes', () { - test('reads all remaining bytes from start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([1, 2, 3, 4, 5])); - expect(reader.availableBytes, equals(0)); - }); - - test('reads remaining bytes after partial read', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - // Read first 2 bytes - ..readUint16(); - - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([3, 4, 5, 6, 7, 8])); - expect(reader.availableBytes, equals(0)); - }); - - test('returns empty list when at end of buffer', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer)..readBytes(3); // Read all bytes - final remaining = reader.readRemainingBytes(); - expect(remaining, isEmpty); - expect(reader.availableBytes, equals(0)); - }); - - test('returns empty list for empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - final remaining = reader.readRemainingBytes(); - expect(remaining, isEmpty); - expect(reader.availableBytes, equals(0)); - }); - - test('is zero-copy operation', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - // Skip first byte - ..readUint8(); - - final remaining = reader.readRemainingBytes(); - // Verify it's a view by checking buffer reference - expect(remaining.buffer, equals(buffer.buffer)); - }); - - test('can be called multiple times at end', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer)..readBytes(3); - - final first = reader.readRemainingBytes(); - final second = reader.readRemainingBytes(); - - expect(first, isEmpty); - expect(second, isEmpty); - }); - - test('works correctly after seek', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(2); - - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([3, 4, 5])); - }); - }); - - group('hasBytes', () { - test('returns true when enough bytes available', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(reader.hasBytes(1), isTrue); - expect(reader.hasBytes(3), isTrue); - expect(reader.hasBytes(5), isTrue); - }); - - test('returns false when not enough bytes available', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(reader.hasBytes(6), isFalse); - expect(reader.hasBytes(10), isFalse); - expect(reader.hasBytes(100), isFalse); - }); - - test('returns true for exact remaining bytes', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes - expect(reader.hasBytes(3), isTrue); // Exactly 3 bytes left - expect(reader.hasBytes(4), isFalse); // Too many - }); - - test('returns true for zero bytes on non-empty buffer', () { - final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer); - - expect(reader.hasBytes(0), isTrue); - }); - - test('returns true for zero bytes on empty buffer', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.hasBytes(0), isTrue); - expect(reader.hasBytes(1), isFalse); - }); - - test('works correctly after reading', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer); - - expect(reader.hasBytes(8), isTrue); - reader.readUint32(); // Read 4 bytes - expect(reader.hasBytes(5), isFalse); - expect(reader.hasBytes(4), isTrue); - reader.readUint32(); // Read 4 more bytes - expect(reader.hasBytes(1), isFalse); - expect(reader.hasBytes(0), isTrue); - }); - - test('does not modify offset', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(reader.offset, equals(0)); - reader.hasBytes(3); - expect(reader.offset, equals(0)); // Offset unchanged - reader.hasBytes(10); - expect(reader.offset, equals(0)); // Still unchanged - }); - - test('works correctly after seek', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(3); - - expect(reader.hasBytes(2), isTrue); - expect(reader.hasBytes(3), isFalse); - expect(reader.offset, equals(3)); // Unchanged - }); - - test('works correctly after rewind', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(4) - ..rewind(2); - - expect(reader.hasBytes(3), isTrue); - expect(reader.hasBytes(4), isFalse); - }); - }); - - group('seek', () { - test('sets position to beginning', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readUint32() // Move to position 4 - ..seek(0); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); - - test('sets position to middle', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(2); - expect(reader.offset, equals(2)); - expect(reader.readUint8(), equals(3)); - }); - - test('sets position to end', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..seek(5); - expect(reader.offset, equals(5)); - expect(reader.availableBytes, equals(0)); - }); - - test('allows seeking backwards', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(4) // Move to position 4 - ..seek(1); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); - }); - - test('allows seeking forwards', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - ..readUint8() // Move to position 1 - ..seek(5); - expect(reader.offset, equals(5)); - expect(reader.readUint8(), equals(6)); - }); - - test('seeking multiple times', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..seek(3); - expect(reader.offset, equals(3)); - reader.seek(1); - expect(reader.offset, equals(1)); - reader.seek(7); - expect(reader.offset, equals(7)); - reader.seek(0); - expect(reader.offset, equals(0)); - }); - - test('seeking to same position is valid', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..seek(2) - ..seek(2); - expect(reader.offset, equals(2)); - }); - - test('throws on negative position', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(() => reader.seek(-1), throwsA(isA())); - }); - - test('throws when seeking beyond buffer', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(() => reader.seek(6), throwsA(isA())); - expect(() => reader.seek(100), throwsA(isA())); - }); - }); - - group('rewind', () { - test('moves back by specified bytes', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(3) // Move to position 3 - ..rewind(2); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); - }); - - test('rewind to beginning', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer) - ..readBytes(3) - ..rewind(3); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); - - test('rewind single byte', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes - expect(reader.offset, equals(2)); - reader.rewind(1); - expect(reader.offset, equals(1)); - expect(reader.readUint8(), equals(2)); - }); - - test('rewind zero bytes does nothing', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); - final offsetBefore = reader.offset; - reader.rewind(0); - expect(reader.offset, equals(offsetBefore)); - }); - - test('allows re-reading data', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); - - final first = reader.readUint32(); - expect(first, equals(0x01020304)); - - reader.rewind(4); - final second = reader.readUint32(); - expect(second, equals(0x01020304)); - expect(second, equals(first)); - }); - - test('multiple rewinds', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..readBytes(5); // Position 5 - expect(reader.offset, equals(5)); - - reader.rewind(2); // Position 3 - expect(reader.offset, equals(3)); - - reader.rewind(1); // Position 2 - expect(reader.offset, equals(2)); - - expect(reader.readUint8(), equals(3)); - }); - - test('rewind and seek together', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer) - ..seek(5) - ..rewind(2); - expect(reader.offset, equals(3)); - - reader.rewind(3); - expect(reader.offset, equals(0)); - }); - - test('throws when rewinding beyond start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint16(); // offset = 2 - - expect(() => reader.rewind(3), throwsA(isA())); - }); - - test('throws when rewinding from start', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); - - expect(() => reader.rewind(1), throwsA(isA())); - }); - - test('throws on negative length', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readBytes(3); - - expect(() => reader.rewind(-1), throwsA(isA())); - }); - }); - - group('VarInt/VarUint edge cases', () { - test('readVarUint with maximum safe 64-bit value boundary', () { - // Test value close to overflow boundary - final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); - }); - - test('readVarInt with maximum positive ZigZag value', () { - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); - }); - - test('readVarInt with minimum negative ZigZag value', () { - final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarInt(), equals(-0x4000000000000000)); - }); - - test('readVarUint boundary values sequence', () { - final writer = BinaryWriter() - ..writeVarUint(0x7F) // 1 byte max - ..writeVarUint(0x80) // 2 byte min - ..writeVarUint(0x3FFF) // 2 byte max - ..writeVarUint(0x4000) // 3 byte min - ..writeVarUint(0x1FFFFF) // 3 byte max - ..writeVarUint(0x200000); // 4 byte min - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(0x7F)); - expect(reader.readVarUint(), equals(0x80)); - expect(reader.readVarUint(), equals(0x3FFF)); - expect(reader.readVarUint(), equals(0x4000)); - expect(reader.readVarUint(), equals(0x1FFFFF)); - expect(reader.readVarUint(), equals(0x200000)); - }); - - test('readVarInt throws on value exceeding int64 range', () { - // Create buffer with VarInt that would decode to value > max int64 - // This tests overflow protection - final buffer = Uint8List.fromList([ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // - 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, // Maximum valid VarInt encoding - ]); - final reader = BinaryReader(buffer); - - // Should successfully read maximum value without throwing - expect( - reader.readVarInt, - returnsNormally, - ); - }); - }); - - group('VarBytes/VarString error handling', () { - test('readVarBytes throws when length exceeds available bytes', () { - // Write VarInt claiming 1000 bytes but only provide 10 - final writer = BinaryWriter() - ..writeVarUint(1000) - ..writeBytes(List.filled(10, 42)); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarBytes, throwsA(isA())); - }); - - test('readVarString throws when length exceeds available bytes', () { - // Write VarInt claiming 100 bytes but only provide 5 - final writer = BinaryWriter() - ..writeVarUint(100) - ..writeBytes([72, 101, 108, 108, 111]); // "Hello" - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarString, throwsA(isA())); - }); - - test('readVarBytes with corrupted length at buffer end', () { - // VarInt that claims more bytes than buffer has - final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = BinaryReader(buffer); - - // Should throw when trying to read the claimed bytes - expect(reader.readVarBytes, throwsA(isA())); - }); - - test('readVarString handles empty string correctly', () { - final writer = BinaryWriter()..writeVarString(''); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('')); - }); - - test('readVarBytes with zero length', () { - final writer = BinaryWriter()..writeVarBytes([]); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarBytes(), isEmpty); - }); - - test('readVarString with malformed UTF-8 in VarString format', () { - // Write invalid UTF-8 sequence with VarInt length prefix - final writer = BinaryWriter() - ..writeVarUint(3) - ..writeBytes([0xFF, 0xFE, 0xFD]); // Invalid UTF-8 - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect( - reader.readVarString, - throwsA(isA()), - ); - - // Reset and try with allowMalformed - final reader2 = BinaryReader(bytes); - final result = reader2.readVarString(allowMalformed: true); - expect(result, isNotEmpty); // Should contain replacement characters - }); - }); - - group('Partial read scenarios', () { - test('reading after partial VarInt consumption', () { - final writer = BinaryWriter() - ..writeVarUint(300) - ..writeUint32(0x12345678); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(300)); - expect(reader.readUint32(), equals(0x12345678)); - expect(reader.availableBytes, equals(0)); - }); - - test('interleaved VarInt and fixed-size reads', () { - final writer = BinaryWriter() - ..writeVarUint(127) - ..writeUint8(42) - ..writeVarInt(-1) - ..writeUint16(1000) - ..writeVarUint(128) - ..writeUint32(0xDEADBEEF); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(127)); - expect(reader.readUint8(), equals(42)); - expect(reader.readVarInt(), equals(-1)); - expect(reader.readUint16(), equals(1000)); - expect(reader.readVarUint(), equals(128)); - expect(reader.readUint32(), equals(0xDEADBEEF)); - }); - - test('readRemainingBytes after VarBytes', () { - final writer = BinaryWriter() - ..writeVarBytes([1, 2, 3]) - ..writeBytes([4, 5, 6, 7, 8]); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - final varBytes = reader.readVarBytes(); - expect(varBytes, equals([1, 2, 3])); - - final remaining = reader.readRemainingBytes(); - expect(remaining, equals([4, 5, 6, 7, 8])); - }); - }); - - group('Navigation edge cases', () { - test('seek and hasBytes combined', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..seek(3); - expect(reader.hasBytes(5), isTrue); - expect(reader.hasBytes(6), isFalse); - - reader.seek(7); - expect(reader.hasBytes(1), isTrue); - expect(reader.hasBytes(2), isFalse); - }); - - test('rewind to exactly zero offset', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readBytes(3); - expect(reader.offset, equals(3)); - - reader.rewind(3); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(1)); - }); - - test('multiple seeks without reading', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer); - - for (var i = 0; i < 8; i++) { - reader.seek(i); - expect(reader.offset, equals(i)); - } - }); - }); - - group('Concise API', () { - test('operator [] returns byte at absolute index', () { - final buffer = Uint8List.fromList([10, 20, 30, 40]); - final reader = BinaryReader(buffer); - expect(reader[0], equals(10)); - expect(reader[1], equals(20)); - expect(reader[2], equals(30)); - expect(reader[3], equals(40)); - expect(reader.offset, equals(0)); - }); - - test('call() is an alias for readBytes', () { - final buffer = Uint8List.fromList([10, 20, 30, 40]); - final reader = BinaryReader(buffer); - final data = reader.call(2); - expect(data, equals([10, 20])); - expect(reader.offset, equals(2)); - }); - }); - - group('Coverage edge cases', () { - test('readVarUint throws on empty buffer', () { - final reader = BinaryReader(Uint8List(0)); - expect(reader.readVarUint, throwsA(isA())); - }); - - test('readVarUint throws on truncated 3-byte value', () { - final reader = BinaryReader(Uint8List.fromList([0x80, 0x80])); - expect(reader.readVarUint, throwsA(isA())); - }); - - test('readVarUint throws on truncated multi-byte value in loop', () { - final reader = BinaryReader( - Uint8List.fromList([0x80, 0x80, 0x80, 0x80]), - ); - expect(reader.readVarUint, throwsA(isA())); - }); - - test('hasBytes throws on negative length', () { - final reader = BinaryReader(Uint8List(10)); - expect(() => reader.hasBytes(-1), throwsA(isA())); - }); - - test('readBytes throws on negative length', () { - final reader = BinaryReader(Uint8List(10)); - expect(() => reader.readBytes(-1), throwsA(isA())); - }); - - test( - 'seek throws on out of range offset in checkBounds via peekBytes', - () { - final reader = BinaryReader(Uint8List(10)); - expect(() => reader.peekBytes(1, 11), throwsA(isA())); - }, - ); - - test('peekByte returns byte at current position without advancing', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.offset, equals(0)); - }); - - test('peekByte after read returns next byte', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])) - ..readUint8(); - expect(reader.peekByte(), equals(0x43)); - expect(reader.offset, equals(1)); - }); - - test('peekByte at end returns last byte', () { - final reader = BinaryReader(Uint8List.fromList([0x42])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.offset, equals(0)); - }); - - test('peekByte multiple times returns same value', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); - expect(reader.peekByte(), equals(0x42)); - expect(reader.peekByte(), equals(0x42)); - expect(reader.peekByte(), equals(0x42)); - }); - - test('fromList creates reader from List', () { - final bytes = [0x01, 0x02, 0x03, 0x04]; - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint8(), equals(1)); - expect(reader.readUint8(), equals(2)); - expect(reader.readUint8(), equals(3)); - expect(reader.readUint8(), equals(4)); - }); - - test('fromList copies data, original list can be modified', () { - final bytes = [0x01, 0x02, 0x03, 0x04]; - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint8(), equals(1)); - bytes[0] = 0xFF; - reader.seek(0); - expect(reader.readUint8(), equals(1)); - }); - - test('fromList works with Uint8List', () { - final bytes = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader.fromList(bytes); - expect(reader.readUint32(), equals(0x01020304)); - }); - - test('fromList with empty list', () { - final reader = BinaryReader.fromList([]); - expect(reader.length, equals(0)); - expect(reader.availableBytes, equals(0)); - expect(reader.readUint8, throwsA(isA())); - }); - }); - - group('rebind', () { - test('rebind replaces buffer and resets offset to 0', () { - final buffer1 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer1)..readUint8(); - expect(reader.offset, equals(1)); - - final buffer2 = Uint8List.fromList([0x10, 0x20, 0x30]); - reader.rebind(buffer2); - - expect(reader.offset, equals(0)); - expect(reader.length, equals(3)); - expect(reader.availableBytes, equals(3)); - expect(reader.readUint8(), equals(0x10)); - expect(reader.readUint8(), equals(0x20)); - }); - - test('rebind after partial reads', () { - final buffer1 = Uint8List.fromList([0x01, 0x02, 0x03, 0x04, 0x05]); - final reader = BinaryReader(buffer1)..readUint32(); - expect(reader.offset, equals(4)); - - final buffer2 = Uint8List.fromList([0xAA, 0xBB]); - reader.rebind(buffer2); - - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(0xAA)); - expect(reader.readUint8(), equals(0xBB)); - }); - - test('rebind with zero-length buffer', () { - final buffer1 = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer1)..readUint8(); - - final emptyBuffer = Uint8List(0); - reader.rebind(emptyBuffer); - - expect(reader.offset, equals(0)); - expect(reader.length, equals(0)); - expect(reader.availableBytes, equals(0)); - expect(reader.readUint8, throwsA(isA())); - }); - - test('rebind preserves reader identity', () { - final buffer1 = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer1); - final readerRef = reader; - - final buffer2 = Uint8List.fromList([0x02, 0x03]); - reader.rebind(buffer2); - - expect(readerRef.length, equals(2)); - expect(readerRef.readUint8(), equals(0x02)); - }); - - test('rebind multiple times', () { - final reader = BinaryReader(Uint8List.fromList([0x01])); - - for (var i = 0; i < 5; i++) { - final buffer = Uint8List.fromList([i, i + 1, i + 2]); - reader.rebind(buffer); - expect(reader.offset, equals(0)); - expect(reader.readUint8(), equals(i)); - } - }); - - test('rebind with data that has non-zero buffer offset', () { - final original = Uint8List.fromList( - List.generate(10, (_) => 0), - ); - final sliced = original.buffer.asUint8List(5, 5); - sliced[0] = 0x42; - sliced[1] = 0x43; - - final reader = BinaryReader(Uint8List.fromList([0x00])) - ..rebind(sliced); - - expect(reader.readUint8(), equals(0x42)); - expect(reader.readUint8(), equals(0x43)); - }); - }); - }); - }); -} diff --git a/test/unit/binary_reader_var_types_test.dart b/test/unit/binary_reader_var_types_test.dart new file mode 100644 index 0000000..04101a2 --- /dev/null +++ b/test/unit/binary_reader_var_types_test.dart @@ -0,0 +1,80 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryReader Variable-Length Types', () { + group('VarInt and VarUint', () { + test('readVarUint basic values', () { + expect(BinaryReader(Uint8List.fromList([0])).readVarUint(), equals(0)); + expect( + BinaryReader(Uint8List.fromList([127])).readVarUint(), + equals(127), + ); + expect( + BinaryReader(Uint8List.fromList([0x80, 0x01])).readVarUint(), + equals(128), + ); + }); + + test('readVarInt (ZigZag) basic values', () { + expect(BinaryReader(Uint8List.fromList([0])).readVarInt(), equals(0)); + expect(BinaryReader(Uint8List.fromList([2])).readVarInt(), equals(1)); + expect(BinaryReader(Uint8List.fromList([1])).readVarInt(), equals(-1)); + }); + + test('readVarUint throws on truncated varint', () { + final buffer = Uint8List.fromList([0x80]); + final reader = BinaryReader(buffer); + expect(reader.readVarUint, throwsA(isA())); + }); + + test('readVarUint throws FormatException on too long varint', () { + final buffer = Uint8List.fromList(List.filled(11, 0x80)); + final reader = BinaryReader(buffer); + expect(reader.readVarUint, throwsA(isA())); + }); + }); + + group('VarBytes', () { + test('readVarBytes basic usage', () { + final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarBytes(), equals([1, 2, 3, 4])); + }); + + test('readVarBytes with empty array', () { + final writer = BinaryWriter()..writeVarBytes([]); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarBytes(), equals([])); + }); + + test('readVarBytes throws on truncated length', () { + final bytes = Uint8List.fromList([0x85]); + final reader = BinaryReader(bytes); + expect(reader.readVarBytes, throwsA(isA())); + }); + }); + + group('VarString', () { + test('readVarString basic usage', () { + final writer = BinaryWriter()..writeVarString('Hello'); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarString(), equals('Hello')); + }); + + test('readVarString with empty string', () { + final writer = BinaryWriter()..writeVarString(''); + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarString(), equals('')); + }); + + test('readVarString throws when not enough data for string', () { + final bytes = Uint8List.fromList([5, 65, 66]); // Length 5, only 2 bytes + final reader = BinaryReader(bytes); + expect(reader.readVarString, throwsA(isA())); + }); + }); + }); +} diff --git a/test/unit/binary_writer_basic_test.dart b/test/unit/binary_writer_basic_test.dart new file mode 100644 index 0000000..3769a12 --- /dev/null +++ b/test/unit/binary_writer_basic_test.dart @@ -0,0 +1,186 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Basic Types', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('initial state is correct', () { + expect(writer.bytesWritten, equals(0)); + expect(writer.takeBytes(), isEmpty); + }); + + test('writes single Uint8 value correctly', () { + writer.writeUint8(1); + expect(writer.takeBytes(), [1]); + }); + + test('writes negative Int8 value correctly', () { + writer.writeInt8(-1); + expect(writer.takeBytes(), [255]); + }); + + test('writes Uint16 in big-endian format', () { + writer.writeUint16(256); + expect(writer.takeBytes(), [1, 0]); + }); + + test('writes Uint16 in little-endian format', () { + writer.writeUint16(256, .little); + expect(writer.takeBytes(), [0, 1]); + }); + + test('writes Int16 in big-endian format', () { + writer.writeInt16(-1); + expect(writer.takeBytes(), [255, 255]); + }); + + test('writes Int16 in little-endian format', () { + writer.writeInt16(-32768, .little); + expect(writer.takeBytes(), [0, 128]); + }); + + test('writes Uint32 in big-endian format', () { + writer.writeUint32(65536); + expect(writer.takeBytes(), [0, 1, 0, 0]); + }); + + test('writes Uint32 in little-endian format', () { + writer.writeUint32(65536, .little); + expect(writer.takeBytes(), [0, 0, 1, 0]); + }); + + test('writes Int32 in big-endian format', () { + writer.writeInt32(-1); + expect(writer.takeBytes(), [255, 255, 255, 255]); + }); + + test('writes Int32 in little-endian format', () { + writer.writeInt32(-2147483648, .little); + expect(writer.takeBytes(), [0, 0, 0, 128]); + }); + + test('writes Uint64 in big-endian format', () { + writer.writeUint64(4294967296); + expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); + }); + + test('writes Uint64 in little-endian format', () { + writer.writeUint64(4294967296, .little); + expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); + }); + + test('writes Int64 in big-endian format', () { + writer.writeInt64(-1); + expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); + }); + + test('writes Int64 in little-endian format', () { + writer.writeInt64(-9223372036854775808, .little); + expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); + }); + + test('writes Float32 in big-endian format', () { + writer.writeFloat32(3.1415927); + expect(writer.takeBytes(), [64, 73, 15, 219]); + }); + + test('writes Float32 in little-endian format', () { + writer.writeFloat32(3.1415927, .little); + expect(writer.takeBytes(), [219, 15, 73, 64]); + }); + + test('writes Float64 in big-endian format', () { + writer.writeFloat64(3.141592653589793); + expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); + }); + + test('writes Float64 in little-endian format', () { + writer.writeFloat64(3.141592653589793, .little); + expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); + }); + + group('writeBool', () { + test('writes true as 0x01', () { + writer.writeBool(true); + expect(writer.takeBytes(), equals([0x01])); + }); + + test('writes false as 0x00', () { + writer.writeBool(false); + expect(writer.takeBytes(), equals([0x00])); + }); + + test('writes multiple boolean values correctly', () { + writer + ..writeBool(true) + ..writeBool(false) + ..writeBool(true) + ..writeBool(true) + ..writeBool(false); + + expect(writer.takeBytes(), equals([0x01, 0x00, 0x01, 0x01, 0x00])); + }); + + test('can be read back with readBool', () { + writer + ..writeBool(true) + ..writeBool(false) + ..writeBool(true); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + }); + + test('updates bytesWritten correctly', () { + expect(writer.bytesWritten, equals(0)); + + writer.writeBool(true); + expect(writer.bytesWritten, equals(1)); + + writer.writeBool(false); + expect(writer.bytesWritten, equals(2)); + + writer.writeBool(true); + expect(writer.bytesWritten, equals(3)); + }); + + test('can be mixed with other write operations', () { + writer + ..writeUint8(42) + ..writeBool(true) + ..writeUint16(258) + ..writeBool(false); + + expect(writer.takeBytes(), equals([42, 0x01, 0x01, 0x02, 0x00])); + }); + }); + + test('allow reusing writer after takeBytes', () { + writer.writeUint8(1); + expect(writer.takeBytes(), [1]); + + writer.writeUint8(2); + expect(writer.takeBytes(), [2]); + }); + + test('track bytesWritten correctly', () { + writer.writeUint8(1); + expect(writer.bytesWritten, equals(1)); + + writer.writeUint16(258); + expect(writer.bytesWritten, equals(3)); + + writer.writeBool(true); + expect(writer.bytesWritten, equals(4)); + }); + }); +} diff --git a/test/unit/binary_writer_buffer_test.dart b/test/unit/binary_writer_buffer_test.dart new file mode 100644 index 0000000..e85a5ec --- /dev/null +++ b/test/unit/binary_writer_buffer_test.dart @@ -0,0 +1,212 @@ +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Buffer Management', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('initial capacity is 128 bytes by default and aligned', () { + expect(writer.capacity, equals(128)); + expect(writer.capacity % 64, equals(0)); + }); + + test('capacity is aligned to 64-byte boundary on initialization', () { + // Test various sizes + final customWriter256 = BinaryWriter(initialBufferSize: 256); + expect(customWriter256.capacity, equals(256)); + expect(customWriter256.capacity % 64, equals(0)); + + // Size 50 should be aligned to 64 + final customWriter50 = BinaryWriter(initialBufferSize: 50); + expect(customWriter50.capacity, equals(64)); + expect(customWriter50.capacity % 64, equals(0)); + + // Size 100 should be aligned to 128 + final customWriter100 = BinaryWriter(initialBufferSize: 100); + expect(customWriter100.capacity, equals(128)); + expect(customWriter100.capacity % 64, equals(0)); + }); + + test('capacity increases after buffer expansion', () { + // Default capacity is 128 bytes + expect(writer.capacity, equals(128)); + + // Write data that exceeds initial capacity + final largeData = Uint8List(200); + writer.writeBytes(largeData); + + // Capacity with 1.5x growth: need 200, 128 * 1.5 = 192 < 200, so use + // 200 aligned to 256 + expect(writer.capacity, equals(256)); + }); + + test('capacity expands with 1.5x growth strategy', () { + final smallWriter = BinaryWriter(initialBufferSize: 64); + expect(smallWriter.capacity, equals(64)); + + // Write 100 bytes (exceeds initial 64) + // 64 * 1.5 = 96 < 100, so use 100 aligned to 128 + smallWriter.writeBytes(Uint8List(100)); + + expect(smallWriter.capacity, equals(128)); + }); + + test('capacity resets to initial size after reset', () { + // Force expansion + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, greaterThan(128)); + + // reset() should reset capacity back to initial size (128) + writer.reset(); + expect(writer.capacity, equals(128)); + expect(writer.bytesWritten, equals(0)); + }); + + test('capacity resets to initial size after takeBytes', () { + // Force expansion + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, greaterThan(128)); + + // takeBytes() resets to initial size (128) + writer.takeBytes(); + expect(writer.capacity, equals(128)); + expect(writer.bytesWritten, equals(0)); + }); + + test('capacity does not change with toBytes', () { + writer.writeBytes(Uint8List(200)); + final capacityBefore = writer.capacity; + + // toBytes() should not change capacity + final bytes = writer.toBytes(); + expect(writer.capacity, equals(capacityBefore)); + expect(bytes.length, equals(200)); + }); + + test('capacity aligns to 64-byte boundary after expansion', () { + // Start with 128 bytes (already aligned to 64) + expect(writer.capacity, equals(128)); + expect(writer.capacity % 64, equals(0)); + + // Write 200 bytes -> requires 256 capacity (128 * 2) + // 256 is already aligned to 64, so capacity should be 256 + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, equals(256)); + expect(writer.capacity % 64, equals(0)); + }); + + test('capacity alignment happens on initialization and expansion', () { + final sizes = [1, 17, 33, 65, 99, 130]; + final expectedInitial = [64, 64, 64, 128, 128, 192]; + + for (var i = 0; i < sizes.length; i++) { + final size = sizes[i]; + final expected = expectedInitial[i]; + final w = BinaryWriter(initialBufferSize: size); + + expect(w.capacity, equals(expected)); + expect(w.capacity % 64, equals(0)); + + w.writeBytes(Uint8List(w.capacity + 1)); + expect(w.capacity % 64, equals(0)); + } + }); + + test('capacity alignment calculation is correct', () { + final testCases = { + 1: 64, + 63: 64, + 64: 64, + 65: 128, + 127: 128, + 128: 128, + 129: 192, + 255: 256, + 256: 256, + 257: 320, + }; + + for (final entry in testCases.entries) { + final unaligned = entry.key; + final aligned = entry.value; + final calculated = (unaligned + 63) & ~63; + expect(calculated, equals(aligned)); + } + }); + + group('toBytes', () { + test('return current buffer without resetting writer state', () { + writer + ..writeUint8(42) + ..writeUint8(100); + + final bytes1 = writer.toBytes(); + expect(bytes1, equals([42, 100])); + + writer.writeUint8(200); + final bytes2 = writer.toBytes(); + expect(bytes2, equals([42, 100, 200])); + }); + + test('preserve written data across toBytes calls', () { + writer.writeUint32(0x12345678); + + final bytes1 = writer.toBytes(); + expect(bytes1, equals([0x12, 0x34, 0x56, 0x78])); + + writer.writeUint32(0xABCDEF00); + + final bytes2 = writer.toBytes(); + expect( + bytes2, + equals([0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x00]), + ); + }); + }); + + group('reset', () { + test('reset writer state without returning bytes', () { + writer + ..writeUint8(42) + ..writeUint8(100) + ..reset(); + + expect(writer.bytesWritten, equals(0)); + expect(writer.toBytes(), isEmpty); + }); + + test('allow writing new data after reset', () { + writer + ..writeUint8(42) + ..reset() + ..writeUint8(100); + + expect(writer.toBytes(), equals([100])); + }); + }); + + group('Memory efficiency', () { + test('takeBytes creates view not copy', () { + writer.writeUint32(0x12345678); + final bytes = writer.takeBytes(); + + expect(bytes, isA()); + expect(bytes.length, equals(4)); + }); + + test('toBytes creates view not copy', () { + writer.writeUint64(123456789); + final bytes = writer.toBytes(); + + expect(bytes, isA()); + expect(bytes.length, equals(8)); + }); + }); + }); +} diff --git a/test/unit/binary_writer_complex_test.dart b/test/unit/binary_writer_complex_test.dart new file mode 100644 index 0000000..5640831 --- /dev/null +++ b/test/unit/binary_writer_complex_test.dart @@ -0,0 +1,81 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Complex Scenarios', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + test('handle complex sequence of different data types', () { + final writer = BinaryWriter() + ..writeUint8(42) + ..writeInt8(-42) + ..writeUint16(65535) + ..writeUint32(4294967295) + ..writeFloat32(3.14) + ..writeBytes([1, 2, 3]); + + final bytes = writer.takeBytes(); + expect(bytes, isNotEmpty); + expect(bytes[0], equals(42)); + }); + + test('full write-read cycle with all types and mixed endianness', () { + writer + ..writeUint8(255) + ..writeInt8(-128) + ..writeUint16(65535) + ..writeInt16(-32768, .little) + ..writeUint32(4294967295, .little) + ..writeInt32(-2147483648) + ..writeFloat32(3.14159, .little) + ..writeString('Hello, 世界! 🌍') + ..writeBytes([1, 2, 3, 4, 5]); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint8(), equals(255)); + expect(reader.readInt8(), equals(-128)); + expect(reader.readUint16(), equals(65535)); + expect(reader.readInt16(.little), equals(-32768)); + expect(reader.readUint32(.little), equals(4294967295)); + expect(reader.readInt32(), equals(-2147483648)); + expect(reader.readFloat32(.little), closeTo(3.14159, 0.00001)); + expect(reader.readString(19), equals('Hello, 世界! 🌍')); + expect(reader.readBytes(5), equals([1, 2, 3, 4, 5])); + }); + + test('complex interleaved writes maintain correct offsets', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeVarUint(300) + ..writeUint16(1000) + ..writeVarInt(-500) + ..writeUint32(0xDEADBEEF) + ..writeVarString('Test') + ..writeBool(true) + ..writeVarBytes([1, 2, 3, 4, 5]) + ..writeFloat32(3.14) + ..writeUint64(123456789); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint8(), equals(1)); + expect(reader.readVarUint(), equals(300)); + expect(reader.readUint16(), equals(1000)); + expect(reader.readVarInt(), equals(-500)); + expect(reader.readUint32(), equals(0xDEADBEEF)); + expect(reader.readVarString(), equals('Test')); + expect(reader.readBool(), isTrue); + expect(reader.readVarBytes(), equals([1, 2, 3, 4, 5])); + expect(reader.readFloat32(), closeTo(3.14, 0.01)); + expect(reader.readUint64(), equals(123456789)); + expect(reader.availableBytes, equals(0)); + }); + }); +} diff --git a/test/unit/binary_writer_edge_cases_test.dart b/test/unit/binary_writer_edge_cases_test.dart new file mode 100644 index 0000000..ff89ead --- /dev/null +++ b/test/unit/binary_writer_edge_cases_test.dart @@ -0,0 +1,93 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Edge Cases and Validation', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + group('Input validation', () { + test('throw RangeError when Uint8 value is negative', () { + expect(() => writer.writeUint8(-1), throwsRangeError); + }); + + test('throw RangeError when Uint8 value exceeds 255', () { + expect(() => writer.writeUint8(256), throwsRangeError); + }); + + test('throw RangeError when Int8 value is out of range', () { + expect(() => writer.writeInt8(-129), throwsRangeError); + expect(() => writer.writeInt8(128), throwsRangeError); + }); + + test('throw RangeError when Uint16 value is out of range', () { + expect(() => writer.writeUint16(-1), throwsRangeError); + expect(() => writer.writeUint16(65536), throwsRangeError); + }); + + test('throw RangeError when Uint32 value is out of range', () { + expect(() => writer.writeUint32(-1), throwsRangeError); + expect(() => writer.writeUint32(4294967296), throwsRangeError); + }); + }); + + group('Edge cases', () { + test('handle empty string correctly', () { + writer.writeString(''); + expect(writer.bytesWritten, equals(0)); + }); + + test('handle empty byte array correctly', () { + writer.writeBytes([]); + expect(writer.bytesWritten, equals(0)); + }); + + test('handle Float32 special values correctly', () { + writer + ..writeFloat32(double.nan) + ..writeFloat32(double.infinity) + ..writeFloat32(double.negativeInfinity); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readFloat32().isNaN, isTrue); + expect(reader.readFloat32(), equals(double.infinity)); + expect(reader.readFloat32(), equals(double.negativeInfinity)); + }); + + test('preserve negative zero in Float64', () { + writer.writeFloat64(-0); + final value = BinaryReader(writer.takeBytes()).readFloat64(); + expect(value, equals(0.0)); + expect(value.isNegative, isTrue); + }); + }); + + group('Boundary values', () { + test('handle maximum values', () { + writer + ..writeUint8(255) + ..writeInt8(127) + ..writeInt8(-128) + ..writeUint16(65535) + ..writeInt32(2147483647); + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint8(), equals(255)); + expect(reader.readInt8(), equals(127)); + expect(reader.readInt8(), equals(-128)); + expect(reader.readUint16(), equals(65535)); + expect(reader.readInt32(), equals(2147483647)); + }); + }); + + group('Concise API', () { + test('call() is an alias for writeBytes', () { + writer([10, 20, 30]); + expect(writer.takeBytes(), equals([10, 20, 30])); + }); + }); + }); +} diff --git a/test/unit/binary_writer_navigation_test.dart b/test/unit/binary_writer_navigation_test.dart new file mode 100644 index 0000000..c6d95c2 --- /dev/null +++ b/test/unit/binary_writer_navigation_test.dart @@ -0,0 +1,58 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Navigation', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + group('seek', () { + test('seeks to position 0', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..seek(0); + expect(writer.bytesWritten, equals(0)); + expect(writer.toBytes(), isEmpty); + }); + + test('seeks to middle position and overwrites', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3) + ..seek(1) + ..writeUint8(99); + expect(writer.toBytes(), equals([1, 99])); + }); + + test('throws RangeError for position beyond bytesWritten', () { + writer.writeUint8(1); + expect(() => writer.seek(2), throwsRangeError); + }); + }); + + group('writeUint8At', () { + test('overwrites byte at middle position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3) + ..writeUint8At(1, 99); + expect(writer.toBytes(), equals([1, 99, 3])); + }); + + test('does not change current write position', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8At(0, 99) + ..writeUint8(3); + expect(writer.toBytes(), equals([99, 2, 3])); + }); + }); + }); +} diff --git a/test/unit/binary_writer_pool_test.dart b/test/unit/binary_writer_pool_test.dart new file mode 100644 index 0000000..3ae827f --- /dev/null +++ b/test/unit/binary_writer_pool_test.dart @@ -0,0 +1,82 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriterPool', () { + setUp(BinaryWriterPool.clear); + + tearDown(BinaryWriterPool.clear); + + test('acquire returns a working writer', () { + final writer = BinaryWriterPool.acquire()..writeUint32(42); + final bytes = writer.toBytes(); + BinaryWriterPool.release(writer); + + expect(bytes, hasLength(4)); + }); + + test('acquire reuses pooled writer', () { + final writer1 = BinaryWriterPool.acquire()..writeUint32(42); + BinaryWriterPool.release(writer1); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + final writer2 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.pooled, equals(0)); + expect(writer2.bytesWritten, equals(0)); // Should be cleared + BinaryWriterPool.release(writer2); + }); + + test('clear empties the pool', () { + final writer1 = BinaryWriterPool.acquire(); + final writer2 = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer1); + BinaryWriterPool.release(writer2); + expect(BinaryWriterPool.stats.pooled, equals(2)); + + BinaryWriterPool.clear(); + expect(BinaryWriterPool.stats.pooled, equals(0)); + }); + + test('discardedLargeBuffers increments when buffer exceeds limit', () { + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); + final writer = BinaryWriterPool.acquire(); + // Write enough data to expand buffer beyond 64 KiB + final largeData = List.filled(70 * 1024, 42); + writer.writeBytes(largeData); + BinaryWriterPool.release(writer); + + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(1)); + expect(BinaryWriterPool.stats.pooled, equals(0)); + }); + + test('withWriter executes action and releases writer', () { + final result = BinaryWriterPool.withWriter((w) { + w.writeUint32(42); + return w.toBytes(); + }); + expect(result, equals([0, 0, 0, 42])); + expect(BinaryWriterPool.stats.pooled, greaterThan(0)); + }); + + test('withWriter releases writer even on error', () { + BinaryWriterPool.clear(); + expect( + () => BinaryWriterPool.withWriter((w) { + throw Exception('Test'); + }), + throwsException, + ); + expect(BinaryWriterPool.stats.pooled, equals(1)); + }); + + test('acquire expands pooled writer if requested size is larger', () { + BinaryWriterPool.clear(); + final writer = BinaryWriterPool.acquire(100); + BinaryWriterPool.release(writer); + + final largerWriter = BinaryWriterPool.acquire(1000); + expect(largerWriter.capacity, greaterThanOrEqualTo(1000)); + BinaryWriterPool.release(largerWriter); + }); + }); +} diff --git a/test/unit/binary_writer_seek_test.dart b/test/unit/binary_writer_seek_test.dart deleted file mode 100644 index 21459d5..0000000 --- a/test/unit/binary_writer_seek_test.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -void main() { - group('BinaryWriter.seek', () { - late BinaryWriter writer; - - setUp(() { - writer = BinaryWriter(); - }); - - test('seeks to position 0', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..seek(0); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('seeks to middle position', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8(4) - ..seek(2) - ..writeUint8(99); - expect(writer.toBytes(), equals([1, 2, 99])); - }); - - test('seeks to end (bytesWritten)', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..seek(2) - ..writeUint8(3); - expect(writer.toBytes(), equals([1, 2, 3])); - }); - - test('throws RangeError for negative position', () { - expect(() => writer.seek(-1), throwsA(isA())); - }); - - test('throws RangeError for position beyond bytesWritten', () { - writer.writeUint8(1); - expect(() => writer.seek(2), throwsA(isA())); - }); - - test('seek and overwrite at beginning', () { - writer - ..writeUint32(0x11223344) - ..writeUint32(0xAABBCCDD) - ..seek(0) - ..writeUint32(0x99887766); - expect(writer.toBytes(), equals([0x99, 0x88, 0x77, 0x66])); - }); - - test('seek preserves bytesWritten after overwrite', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..seek(1) - ..writeUint8(99); - expect(writer.bytesWritten, equals(2)); - }); - }); - - group('BinaryWriter.writeUint8At', () { - late BinaryWriter writer; - - setUp(() { - writer = BinaryWriter(); - }); - - test('overwrites byte at position 0', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8At(0, 99); - expect(writer.toBytes(), equals([99, 2, 3])); - }); - - test('overwrites byte at middle position', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8At(1, 99); - expect(writer.toBytes(), equals([1, 99, 3])); - }); - - test('overwrites byte at last position', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8At(2, 99); - expect(writer.toBytes(), equals([1, 2, 99])); - }); - - test('does not change bytesWritten', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8At(0, 99); - expect(writer.bytesWritten, equals(2)); - }); - - test('does not change current write position', () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3) - ..writeUint8At(1, 99) - ..writeUint8(4); - expect(writer.toBytes(), equals([1, 99, 3, 4])); - }); - - test('throws RangeError for negative position', () { - writer.writeUint8(1); - expect(() => writer.writeUint8At(-1, 0), throwsA(isA())); - }); - - test('throws RangeError for position beyond bytesWritten', () { - writer.writeUint8(1); - expect(() => writer.writeUint8At(2, 0), throwsA(isA())); - }); - - test('throws RangeError for value exceeding 255', () { - expect(() => writer.writeUint8At(0, 256), throwsA(isA())); - }); - - test('throws RangeError for negative value', () { - expect(() => writer.writeUint8At(0, -1), throwsA(isA())); - }); - - test('works on empty writer at position 0', () { - writer.writeUint8At(0, 42); - expect(writer.bytesWritten, equals(1)); - expect(writer.toBytes(), equals([42])); - }); - }); - - group('BinaryWriter.seek + writeVarString integration', () { - late BinaryWriter writer; - - setUp(() { - writer = BinaryWriter(); - }); - - test('writeVarString uses seek internally', () { - writer.writeVarString('hello'); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('hello')); - }); - - test('writeVarString with non-ASCII uses seek for VarInt rewrite', () { - writer.writeVarString('Привет'); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('Привет')); - }); - - test('writeVarString with emoji uses seek for VarInt rewrite', () { - writer.writeVarString('🚀🌍'); - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals('🚀🌍')); - }); - }); -} diff --git a/test/unit/binary_writer_string_test.dart b/test/unit/binary_writer_string_test.dart new file mode 100644 index 0000000..ba36b9e --- /dev/null +++ b/test/unit/binary_writer_string_test.dart @@ -0,0 +1,312 @@ +import 'dart:convert'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter String Operations', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + group('UTF-8 encoding', () { + test('encode ASCII characters correctly', () { + writer.writeString('ABC123'); + expect(writer.takeBytes(), equals([65, 66, 67, 49, 50, 51])); + }); + + test('encode Cyrillic characters correctly', () { + writer.writeString('Привет'); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals('Привет')); + }); + + test('encode Chinese characters correctly', () { + const str = '你好世界'; + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals(str)); + }); + + test('encode mixed Unicode string correctly', () { + const str = 'Hello мир 世界 🌍'; + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals(str)); + }); + }); + + group('Lone surrogate pairs', () { + test( + 'writeString handles lone high surrogate with allowMalformed=true', + () { + const testStr = 'Before\uD800After'; + writer.writeString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length, allowMalformed: true); + expect(result, isNotEmpty); + expect(result, contains('Before')); + expect(result, contains('After')); + expect(result.contains('\uFFFD') || result.contains(''), isTrue); + }, + ); + + test( + 'writeString throws on lone high surrogate with allowMalformed=false', + () { + const testStr = 'Before\uD800After'; + expect( + () => writer.writeString(testStr, allowMalformed: false), + throwsA(isA()), + ); + }, + ); + + test( + 'writeString handles lone low surrogate with allowMalformed=true', + () { + const testStr = 'Before\uDC00After'; + writer.writeString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length, allowMalformed: true); + expect(result, isNotEmpty); + expect(result, contains('Before')); + expect(result, contains('After')); + expect(result.contains('\uFFFD') || result.contains(''), isTrue); + }, + ); + + test( + 'writeString throws on lone low surrogate with allowMalformed=false', + () { + const testStr = 'Before\uDC00After'; + expect( + () => writer.writeString(testStr, allowMalformed: false), + throwsA(isA()), + ); + }, + ); + + test('writeString handles valid surrogate pair', () { + const testStr = 'Test\u{1F600}End'; + writer.writeString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length); + expect(result, equals(testStr)); + }); + + test('writeString handles mixed valid and invalid surrogates', () { + const testStr = 'A\u{1F600}B\uD800C'; + writer.writeString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length, allowMalformed: true); + expect(result, contains('A')); + expect(result, contains('B')); + expect(result, contains('C')); + expect(result.contains('\uFFFD') || result.contains(''), isTrue); + }); + + test( + 'writeString throws on mixed surrogates with allowMalformed=false', + () { + const testStr = 'A\u{1F600}B\uD800C'; + expect( + () => writer.writeString(testStr, allowMalformed: false), + throwsA(isA()), + ); + }, + ); + }); + + group('Very large strings', () { + test('writeString with string exceeding initial buffer size', () { + final writer = BinaryWriter(initialBufferSize: 8); + const largeString = + 'This is a very long string that exceeds initial' + ' buffer size and should trigger buffer expansion properly'; + + writer.writeString(largeString); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length); + expect(result, equals(largeString)); + }); + + test('writeString with string requiring more than 1.5x growth', () { + final writer = BinaryWriter(initialBufferSize: 4); + const str = 'Very long string to force larger growth'; + + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length); + expect(result, equals(str)); + }); + + test('writeString with multi-byte UTF-8 characters exceeding buffer', () { + final writer = BinaryWriter(initialBufferSize: 8); + const str = 'Привет мир! Это длинная строка для теста'; + + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length); + expect(result, equals(str)); + }); + + test('writeString with Chinese characters requiring buffer growth', () { + final writer = BinaryWriter(initialBufferSize: 16); + const str = '这是一个非常长的中文字符串用于测试缓冲区扩展功能是否正常工作'; + + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length); + expect(result, equals(str)); + }); + }); + + group('getUtf8Length function', () { + test('with ASCII only', () { + expect(getUtf8Length('Hello'), equals(5)); + expect(getUtf8Length('ABCDEFGH'), equals(8)); // Fast path + }); + + test('with empty string', () { + expect(getUtf8Length(''), equals(0)); + }); + + test('with 2-byte UTF-8 chars', () { + expect(getUtf8Length('café'), equals(5)); // 'caf' = 3, 'é' = 2 + expect(getUtf8Length('Привет'), equals(12)); // Each Cyrillic = 2 bytes + }); + + test('with 3-byte UTF-8 chars', () { + expect(getUtf8Length('世界'), equals(6)); // Each Chinese = 3 bytes + expect(getUtf8Length('你好'), equals(6)); + }); + + test('with 4-byte UTF-8 chars (emoji)', () { + expect(getUtf8Length('🌍'), equals(4)); + expect(getUtf8Length('🎉'), equals(4)); + expect(getUtf8Length('😀'), equals(4)); + }); + + test('with mixed content', () { + // 'Hello' = 5, ', ' = 2, '世界' = 6, '! ' = 2, '🌍' = 4 + expect(getUtf8Length('Hello, 世界! 🌍'), equals(19)); + }); + + test('matches actual UTF-8 encoding', () { + final strings = [ + 'Test', + 'Тест', + '测试', + '🧪', + 'Mix テスト 123', + 'A' * 100, // Long ASCII for fast path + ]; + + for (final str in strings) { + final calculated = getUtf8Length(str); + final actual = utf8.encode(str).length; + expect( + calculated, + equals(actual), + reason: 'Failed for string: "$str"', + ); + } + }); + + test('with surrogate pairs', () { + // Valid surrogate pair forms emoji + final emoji = String.fromCharCodes([0xD83C, 0xDF0D]); // 🌍 + expect(getUtf8Length(emoji), equals(4)); + }); + + test('with malformed high surrogate', () { + // High surrogate (0xD800-0xDBFF) not followed by low surrogate + // This triggers the malformed surrogate pair path in getUtf8Length + final malformed = String.fromCharCodes([ + 0xD800, + 0x0041, + ]); // High surrogate + 'A' + expect( + getUtf8Length(malformed), + equals(4), + ); // 3 bytes (replacement) + 1 byte (A) + }); + + test('with lone high surrogate at end', () { + // High surrogate at the end of string (also malformed) + final malformed = String.fromCharCodes([ + 0x0041, + 0xD800, + ]); // 'A' + high surrogate + expect( + getUtf8Length(malformed), + equals(4), + ); // 1 byte (A) + 3 bytes (replacement) + }); + }); + + group('Special UTF-8 cases', () { + test('writeString with only ASCII (fast path)', () { + const str = 'OnlyASCII123'; + writer.writeString(str); + final bytes = writer.takeBytes(); + + expect(bytes.length, equals(str.length)); + }); + + test('writeString with mixed ASCII and multi-byte', () { + const str = 'ASCII_Юникод_中文'; + writer.writeString(str); + final bytes = writer.takeBytes(); + + expect(bytes.length, greaterThan(str.length)); + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals(str)); + }); + + test('writeString with only 4-byte characters (emojis)', () { + const str = '🚀🌟💻🎉🔥'; + writer.writeString(str); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals(str)); + }); + + test('writeString empty string after previous writes', () { + writer + ..writeUint8(42) + ..writeString('') + ..writeUint8(43); + + final bytes = writer.takeBytes(); + expect(bytes, equals([42, 43])); + }); + }); + }); +} diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart deleted file mode 100644 index 50e37ff..0000000 --- a/test/unit/binary_writer_test.dart +++ /dev/null @@ -1,2931 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -void main() { - group('BinaryWriter', () { - late BinaryWriter writer; - - setUp(() { - writer = BinaryWriter(); - }); - - test('throws RangeError when initialBufferSize is not positive', () { - expect( - () => BinaryWriter(initialBufferSize: 0), - throwsA( - isA().having((e) => e.name, 'name', 'initialBufferSize'), - ), - ); - }); - - test('returns empty list when takeBytes called on empty writer', () { - expect(writer.takeBytes(), isEmpty); - }); - - test('writes single Uint8 value correctly', () { - writer.writeUint8(1); - expect(writer.takeBytes(), [1]); - }); - - test('writes negative Int8 value correctly', () { - writer.writeInt8(-1); - expect(writer.takeBytes(), [255]); - }); - - test('writes Uint16 in big-endian format', () { - writer.writeUint16(256); - expect(writer.takeBytes(), [1, 0]); - }); - - test('writes Uint16 in little-endian format', () { - writer.writeUint16(256, .little); - expect(writer.takeBytes(), [0, 1]); - }); - - test('writes Int16 in big-endian format', () { - writer.writeInt16(-1); - expect(writer.takeBytes(), [255, 255]); - }); - - test('writes Int16 in little-endian format', () { - writer.writeInt16(-32768, .little); - expect(writer.takeBytes(), [0, 128]); - }); - - test('writes Uint32 in big-endian format', () { - writer.writeUint32(65536); - expect(writer.takeBytes(), [0, 1, 0, 0]); - }); - - test('writes Uint32 in little-endian format', () { - writer.writeUint32(65536, .little); - expect(writer.takeBytes(), [0, 0, 1, 0]); - }); - - test('writes Int32 in big-endian format', () { - writer.writeInt32(-1); - expect(writer.takeBytes(), [255, 255, 255, 255]); - }); - - test('writes Int32 in little-endian format', () { - writer.writeInt32(-2147483648, .little); - expect(writer.takeBytes(), [0, 0, 0, 128]); - }); - - test('writes Uint64 in big-endian format', () { - writer.writeUint64(4294967296); - expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); - }); - - test('writes Uint64 in little-endian format', () { - writer.writeUint64(4294967296, .little); - expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); - }); - - test('writes Int64 in big-endian format', () { - writer.writeInt64(-1); - expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); - }); - - test('writes Int64 in little-endian format', () { - writer.writeInt64(-9223372036854775808, .little); - expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); - }); - - test('writes Float32 in big-endian format', () { - writer.writeFloat32(3.1415927); - expect(writer.takeBytes(), [64, 73, 15, 219]); - }); - - test('writes Float32 in little-endian format', () { - writer.writeFloat32(3.1415927, .little); - expect(writer.takeBytes(), [219, 15, 73, 64]); - }); - - test('writes Float64 in big-endian format', () { - writer.writeFloat64(3.141592653589793); - expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); - }); - - test('writes Float64 in little-endian format', () { - writer.writeFloat64(3.141592653589793, .little); - expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); - }); - - test('writes VarInt single byte (0)', () { - writer.writeVarUint(0); - expect(writer.takeBytes(), [0]); - }); - - test('writes VarInt single byte (127)', () { - writer.writeVarUint(127); - expect(writer.takeBytes(), [127]); - }); - - test('writes VarInt two bytes (128)', () { - writer.writeVarUint(128); - expect(writer.takeBytes(), [0x80, 0x01]); - }); - - test('writes VarInt two bytes (300)', () { - writer.writeVarUint(300); - expect(writer.takeBytes(), [0xAC, 0x02]); - }); - - test('writes VarInt three bytes (16384)', () { - writer.writeVarUint(16384); - expect(writer.takeBytes(), [0x80, 0x80, 0x01]); - }); - - test('writes VarInt four bytes (2097151)', () { - writer.writeVarUint(2097151); - expect(writer.takeBytes(), [0xFF, 0xFF, 0x7F]); - }); - - test('writes VarInt five bytes (268435455)', () { - writer.writeVarUint(268435455); - expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0x7F]); - }); - - test('writes VarInt large value', () { - writer.writeVarUint(1 << 30); - expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); - }); - - test('writes ZigZag encoding for positive values', () { - writer.writeVarInt(0); - expect(writer.takeBytes(), [0]); - }); - - test('writes ZigZag encoding for positive value 1', () { - writer.writeVarInt(1); - expect(writer.takeBytes(), [2]); - }); - - test('writes ZigZag encoding for negative value -1', () { - writer.writeVarInt(-1); - expect(writer.takeBytes(), [1]); - }); - - test('writes ZigZag encoding for positive value 2', () { - writer.writeVarInt(2); - expect(writer.takeBytes(), [4]); - }); - - test('writes ZigZag encoding for negative value -2', () { - writer.writeVarInt(-2); - expect(writer.takeBytes(), [3]); - }); - - test('writes ZigZag encoding for large positive value', () { - writer.writeVarInt(2147483647); - expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); - }); - - test('writes ZigZag encoding for large negative value', () { - writer.writeVarInt(-2147483648); - expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); - }); - - test('writeVarUint fast path boundary: 0', () { - // 0 is unsigned and should use fast path (single byte) - writer.writeVarUint(0); - expect(writer.takeBytes(), [0]); - }); - - test('writeVarUint fast path boundary: 127 (max single byte)', () { - // 127 (0x7F) is the last value where MSB is not set - writer.writeVarUint(127); - expect(writer.takeBytes(), [127]); - }); - - test('writeVarUint multi-byte boundary: 128 (min two bytes)', () { - // 128 (0x80) requires 2 bytes because MSB is set - writer.writeVarUint(128); - expect(writer.takeBytes(), [0x80, 0x01]); - }); - - test('writeVarInt fast path: ZigZag encodes small values correctly', () { - // ZigZag(0) = 0 → single byte - writer.writeVarInt(0); - expect(writer.toBytes(), [0]); - - writer - ..reset() - // ZigZag(1) = 2 → single byte - ..writeVarInt(1); - expect(writer.toBytes(), [2]); - - writer - ..reset() - // ZigZag(-1) = 1 → single byte - ..writeVarInt(-1); - expect(writer.toBytes(), [1]); - }); - - test('writeVarInt multi-byte: ZigZag crosses boundary correctly', () { - // ZigZag(64) = 128 → requires 2 bytes (MSB set) - writer.writeVarInt(64); - expect(writer.takeBytes(), [0x80, 0x01]); - - // ZigZag(-64) = 127 → single byte - writer.writeVarInt(-64); - expect(writer.takeBytes(), [127]); - - // ZigZag(-65) = 129 → requires 2 bytes - writer.writeVarInt(-65); - expect(writer.takeBytes(), [0x81, 0x01]); - }); - - test( - 'writeVarUint with negative value must not use fast path ' - '(regression test)', - () { - // CRITICAL: writeVarUint(-1) must NOT use fast path - // Negative numbers: -1 as bits = 0xFFFFFFFF... - // -1 < 0x80 is FALSE, so it should use slow path - // This verifies the `value >= 0` check is necessary - - writer.writeVarUint(-1); - final bytes = writer.takeBytes(); - - // Without `value >= 0` check, -1 might be incorrectly encoded as 1 byte - // With check: -1 triggers slow path and encodes as 10 bytes - expect( - bytes.length, - 10, - reason: 'Negative number should use multi-byte path', - ); - expect( - bytes[0], - 0xFF, - reason: 'First byte should have continuation bit set', - ); - expect(bytes[9], 0x01, reason: 'Last byte should be continuation end'); - }, - ); - - test('write byte array correctly', () { - writer.writeBytes([1, 2, 3, 4, 5]); - expect(writer.takeBytes(), [1, 2, 3, 4, 5]); - }); - - test('encode string to UTF-8 bytes correctly', () { - writer.writeString('Hello, World!'); - expect(writer.takeBytes(), [ - 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, // ASCII - ]); - }); - - test('handle complex sequence of different data types', () { - final writer = BinaryWriter() - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535) - ..writeInt16(-32768) - ..writeUint32(4294967295) - ..writeInt32(-2147483648) - ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808) - ..writeFloat32(3.14) - ..writeFloat64(3.141592653589793) - ..writeBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255]); - - final bytes = writer.takeBytes(); - - final expectedBytes = [ - 42, // Uint8 - 214, // Int8 (two's complement of -42 is 214) - 255, 255, // Uint16 (65535 in big endian) - 128, 0, // Int16 (-32768 in big endian) - 255, 255, 255, 255, // Uint32 (4294967295 in big endian) - 128, 0, 0, 0, // Int32 (-2147483648 in big endian) - 127, 255, 255, 255, 255, 255, 255, - 255, // Uint64 (9223372036854775807 in big endian) - 128, 0, 0, 0, 0, 0, 0, 0, // Int64 (-9223372036854775808 in big endian) - 64, 72, 245, 195, // Float32 (3.14 in IEEE 754 format, big endian) - 64, 9, 33, 251, 84, 68, 45, - 24, // Float64 (3.141592653589793 in IEEE 754 format, big endian) - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, // Bytes - ]; - - expect(bytes, equals(expectedBytes)); - }); - - test( - 'should automatically expand buffer when size exceeds initial capacity', - () { - for (var i = 0; i < 100; i++) { - writer.writeUint8(i); - } - - final result = writer.takeBytes(); - expect(result.length, equals(100)); - for (var i = 0; i < 100; i++) { - expect(result[i], equals(i)); - } - }, - ); - - test('allow reusing writer after takeBytes', () { - writer.writeUint8(1); - expect(writer.takeBytes(), [1]); - - writer.writeUint8(2); - expect(writer.takeBytes(), [2]); - }); - - test('handle writing large data sets efficiently', () { - final largeData = Uint8List.fromList( - List.generate(10000, (i) => i % 256), - ); - - writer.writeBytes(largeData); - - final result = writer.takeBytes(); - - expect(result.length, equals(10000)); - expect(result, equals(largeData)); - }); - - test('track bytesWritten correctly', () { - writer.writeUint8(1); - expect(writer.bytesWritten, equals(1)); - - writer.writeUint16(258); - expect(writer.bytesWritten, equals(3)); - - writer.writeBytes([1, 2, 3, 4]); - expect(writer.bytesWritten, equals(7)); - - // Test with a large amount of data written - final largeData = Uint8List.fromList( - List.generate(10000, (i) => i % 256), - ); - writer.writeBytes(largeData); - expect(writer.bytesWritten, equals(10007)); - }); - - test('initial capacity is 128 bytes by default and aligned', () { - expect(writer.capacity, equals(128)); - expect(writer.capacity % 64, equals(0)); - }); - - test('capacity is aligned to 64-byte boundary on initialization', () { - // Test various sizes - final customWriter256 = BinaryWriter(initialBufferSize: 256); - expect(customWriter256.capacity, equals(256)); - expect(customWriter256.capacity % 64, equals(0)); - - // Size 50 should be aligned to 64 - final customWriter50 = BinaryWriter(initialBufferSize: 50); - expect(customWriter50.capacity, equals(64)); - expect(customWriter50.capacity % 64, equals(0)); - - // Size 100 should be aligned to 128 - final customWriter100 = BinaryWriter(initialBufferSize: 100); - expect(customWriter100.capacity, equals(128)); - expect(customWriter100.capacity % 64, equals(0)); - }); - - test('capacity increases after buffer expansion', () { - // Default capacity is 128 bytes - expect(writer.capacity, equals(128)); - - // Write data that exceeds initial capacity - final largeData = Uint8List(200); - writer.writeBytes(largeData); - - // Capacity with 1.5x growth: need 200, 128 * 1.5 = 192 < 200, so use - // 200 aligned to 256 - expect(writer.capacity, equals(256)); - }); - - test('capacity expands with 1.5x growth strategy', () { - final smallWriter = BinaryWriter(initialBufferSize: 64); - expect(smallWriter.capacity, equals(64)); - - // Write 100 bytes (exceeds initial 64) - // 64 * 1.5 = 96 < 100, so use 100 aligned to 128 - smallWriter.writeBytes(Uint8List(100)); - - expect(smallWriter.capacity, equals(128)); - }); - - test('capacity resets to initial size after reset', () { - // Force expansion - writer.writeBytes(Uint8List(200)); - expect(writer.capacity, greaterThan(128)); - - // reset() should reset capacity back to initial size (128) - writer.reset(); - expect(writer.capacity, equals(128)); - expect(writer.bytesWritten, equals(0)); - }); - - test('capacity resets to initial size after takeBytes', () { - // Force expansion - writer.writeBytes(Uint8List(200)); - expect(writer.capacity, greaterThan(128)); - - // takeBytes() resets to initial size (128) - writer.takeBytes(); - expect(writer.capacity, equals(128)); - expect(writer.bytesWritten, equals(0)); - }); - - test('capacity does not change with toBytes', () { - writer.writeBytes(Uint8List(200)); - final capacityBefore = writer.capacity; - - // toBytes() should not change capacity - final bytes = writer.toBytes(); - expect(writer.capacity, equals(capacityBefore)); - expect(bytes.length, equals(200)); - }); - - test('capacity aligns to 64-byte boundary after expansion', () { - // Start with 128 bytes (already aligned to 64) - expect(writer.capacity, equals(128)); - expect(writer.capacity % 64, equals(0)); - - // Write 200 bytes -> requires 256 capacity (128 * 2) - // 256 is already aligned to 64, so capacity should be 256 - writer.writeBytes(Uint8List(200)); - expect(writer.capacity, equals(256)); - expect(writer.capacity % 64, equals(0)); - }); - - test('capacity aligns to 64-byte boundary from small initial size', () { - // Start with 50 bytes -> aligned to 64 - final smallWriter = BinaryWriter(initialBufferSize: 50); - expect(smallWriter.capacity, equals(64)); - - // Write 100 bytes -> 64 * 2 = 128 (aligned) - smallWriter.writeBytes(Uint8List(100)); - expect(smallWriter.capacity, equals(128)); - expect(smallWriter.capacity % 64, equals(0)); - }); - - test('capacity alignment happens on initialization and expansion', () { - // Test that both initialization and expansion align to 64-byte boundary - final sizes = [1, 17, 33, 65, 99, 130]; - final expectedInitial = [64, 64, 64, 128, 128, 192]; - - for (var i = 0; i < sizes.length; i++) { - final size = sizes[i]; - final expected = expectedInitial[i]; - final w = BinaryWriter(initialBufferSize: size); - - // Initial capacity should be aligned - expect( - w.capacity, - equals(expected), - reason: 'Initial size $size should align to $expected', - ); - expect( - w.capacity % 64, - equals(0), - reason: 'Initial capacity should be aligned', - ); - - // After expansion, capacity should still be aligned - w.writeBytes(Uint8List(w.capacity + 1)); - expect( - w.capacity % 64, - equals(0), - reason: 'Capacity after expansion should be aligned', - ); - } - }); - - test('capacity expansion maintains 64-byte alignment', () { - // Start with 64 bytes - final w = BinaryWriter(initialBufferSize: 64); - expect(w.capacity, equals(64)); - - // Force multiple expansions - w.writeBytes( - Uint8List(100), - ); // Need 100, 64 * 1.5 = 96 < 100, so use 100 aligned to 128 - expect(w.capacity % 64, equals(0)); - expect(w.capacity, equals(128)); - // Total 250, need 250: 128 * 1.5 = 192 < 250, so use 250 aligned to 256 - w.writeBytes(Uint8List(150)); - expect(w.capacity % 64, equals(0)); - expect(w.capacity, equals(256)); - }); - - test('capacity with exact requirement uses alignment', () { - final w = BinaryWriter(initialBufferSize: 64); - expect(w.capacity, equals(64)); // Already aligned - - // Write exactly 65 bytes -> need 65 total capacity - // Current: 64, need: 65, so expand: 64 * 1.5 = 96, aligned to 64 = 128 - w.writeBytes(Uint8List(65)); - expect(w.capacity, equals(128)); - expect(w.capacity % 64, equals(0)); - - // Now write 65 more bytes -> total written: 130, need 130 capacity - // Current: 128, need: 130, so expand: 128 * 1.5 = 192 (already aligned) - w.writeBytes(Uint8List(65)); - expect(w.capacity, equals(192)); - expect(w.capacity % 64, equals(0)); - }); - - test('capacity alignment calculation is correct', () { - // Test specific alignment calculations - final testCases = { - 1: 64, // (1 + 63) & ~63 = 64 - 63: 64, // (63 + 63) & ~63 = 64 - 64: 64, // (64 + 63) & ~63 = 64 - 65: 128, // (65 + 63) & ~63 = 128 - 127: 128, // (127 + 63) & ~63 = 128 - 128: 128, // (128 + 63) & ~63 = 128 - 129: 192, // (129 + 63) & ~63 = 192 - 255: 256, // (255 + 63) & ~63 = 256 - 256: 256, // (256 + 63) & ~63 = 256 - 257: 320, // (257 + 63) & ~63 = 320 - }; - - for (final entry in testCases.entries) { - final unaligned = entry.key; - final aligned = entry.value; - final calculated = (unaligned + 63) & ~63; - expect( - calculated, - equals(aligned), - reason: 'Alignment of $unaligned should be $aligned', - ); - } - }); - - group('Input validation', () { - test('throw RangeError when Uint8 value is negative', () { - expect( - () => writer.writeUint8(-1), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint8') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 255), - ), - ); - }); - - test('throw RangeError when Uint8 value exceeds 255', () { - expect( - () => writer.writeUint8(256), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint8') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 255), - ), - ); - }); - - test('throw RangeError when Int8 value is less than -128', () { - expect( - () => writer.writeInt8(-129), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int8') - .having((e) => e.start, 'start', -128) - .having((e) => e.end, 'end', 127), - ), - ); - }); - - test('throw RangeError when Int8 value exceeds 127', () { - expect( - () => writer.writeInt8(128), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int8') - .having((e) => e.start, 'start', -128) - .having((e) => e.end, 'end', 127), - ), - ); - }); - - test('throw RangeError when Uint16 value is negative', () { - expect( - () => writer.writeUint16(-1), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint16') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 65535), - ), - ); - }); - - test('throw RangeError when Uint16 value exceeds 65535', () { - expect( - () => writer.writeUint16(65536), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint16') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 65535), - ), - ); - }); - - test( - 'should throw RangeError when Int16 value is less than -32768', - () { - expect( - () => writer.writeInt16(-32769), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int16') - .having((e) => e.start, 'start', -32768) - .having((e) => e.end, 'end', 32767), - ), - ); - }, - ); - - test('throw RangeError when Int16 value exceeds 32767', () { - expect( - () => writer.writeInt16(32768), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int16') - .having((e) => e.start, 'start', -32768) - .having((e) => e.end, 'end', 32767), - ), - ); - }); - - test('throw RangeError when Uint32 value is negative', () { - expect( - () => writer.writeUint32(-1), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint32') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 4294967295), - ), - ); - }); - - test( - 'should throw RangeError when Uint32 value exceeds 4294967295', - () { - expect( - () => writer.writeUint32(4294967296), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint32') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 4294967295), - ), - ); - }, - ); - - test( - 'should throw RangeError when Int32 value is less than -2147483648', - () { - expect( - () => writer.writeInt32(-2147483649), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int32') - .having((e) => e.start, 'start', -2147483648) - .having((e) => e.end, 'end', 2147483647), - ), - ); - }, - ); - - test( - 'should throw RangeError when Int32 value exceeds 2147483647', - () { - expect( - () => writer.writeInt32(2147483648), - throwsA( - isA() - .having((e) => e.name, 'name', 'Int32') - .having((e) => e.start, 'start', -2147483648) - .having((e) => e.end, 'end', 2147483647), - ), - ); - }, - ); - }); - - group('toBytes', () { - test('return current buffer without resetting writer state', () { - writer - ..writeUint8(42) - ..writeUint8(100); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([42, 100])); - - // Should not reset, can continue writing - writer.writeUint8(200); - final bytes2 = writer.toBytes(); - expect(bytes2, equals([42, 100, 200])); - }); - - test( - 'should behave differently from takeBytes ' - '(toBytes preserves state, takeBytes resets)', - () { - writer - ..writeUint8(1) - ..writeUint8(2); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([1, 2])); - - // takeBytes should reset - final bytes2 = writer.takeBytes(); - expect(bytes2, equals([1, 2])); - - // After takeBytes, should be empty - final bytes3 = writer.toBytes(); - expect(bytes3, isEmpty); - }, - ); - - test('return empty list when called on empty writer', () { - final bytes = writer.toBytes(); - expect(bytes, isEmpty); - }); - }); - - group('clear', () { - test('reset writer state without returning bytes', () { - writer - ..writeUint8(42) - ..writeUint8(100) - ..reset(); - - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('allow writing new data after reset', () { - writer - ..writeUint8(42) - ..reset() - ..writeUint8(100); - - expect(writer.toBytes(), equals([100])); - }); - - test('be safe to call on empty writer', () { - writer.reset(); - expect(writer.bytesWritten, equals(0)); - }); - }); - - group('Edge cases', () { - test('handle empty string correctly', () { - writer.writeString(''); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('handle empty byte array correctly', () { - writer.writeBytes([]); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('encode emoji characters correctly', () { - const str = '🚀👨‍👩‍👧‍👦'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('handle Float32 NaN value correctly', () { - writer.writeFloat32(.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('handle Float32 positive Infinity correctly', () { - writer.writeFloat32(.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.infinity)); - }); - - test('handle Float32 negative Infinity correctly', () { - writer.writeFloat32(.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.negativeInfinity)); - }); - - test('handle Float64 NaN value correctly', () { - writer.writeFloat64(.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64().isNaN, isTrue); - }); - - test('handle Float64 positive Infinity correctly', () { - writer.writeFloat64(.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.infinity)); - }); - - test('handle Float64 negative Infinity correctly', () { - writer.writeFloat64(.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }); - - test('preserve negative zero in Float64', () { - writer.writeFloat64(-0); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final value = reader.readFloat64(); - expect(value, equals(0.0)); - expect(value.isNegative, isTrue); - }); - - test('throw RangeError when Uint64 value is negative', () { - expect( - () => writer.writeUint64(-1), - throwsA( - isA() - .having((e) => e.name, 'name', 'Uint64') - .having((e) => e.start, 'start', 0) - .having((e) => e.end, 'end', 9223372036854775807), - ), - ); - }); - - test( - 'should correctly expand buffer when exceeding initial capacity by ' - 'one byte', - () { - final writer = BinaryWriter(initialBufferSize: 8) - // Write exactly 8 bytes - ..writeUint64(42); - expect(writer.bytesWritten, equals(8)); - - // Writing one more byte should trigger expansion - writer.writeUint8(1); - expect(writer.bytesWritten, equals(9)); - - final bytes = writer.takeBytes(); - expect(bytes.length, equals(9)); - }, - ); - - test('handle multiple consecutive reset calls', () { - writer - ..writeUint8(42) - ..reset() - ..reset() - ..reset(); - - expect(writer.bytesWritten, equals(0)); - }); - - test('support method chaining after reset', () { - writer - ..writeUint8(1) - ..reset() - ..writeUint8(2) - ..writeUint8(3); - - expect(writer.toBytes(), equals([2, 3])); - }); - }); - - group('Boundary values - Maximum', () { - test('handle Uint8 maximum value (255)', () { - writer.writeUint8(255); - expect(writer.takeBytes(), equals([255])); - }); - - test('handle Int8 maximum positive value (127)', () { - writer.writeInt8(127); - expect(writer.takeBytes(), equals([127])); - }); - - test('handle Int8 minimum negative value (-128)', () { - writer.writeInt8(-128); - expect(writer.takeBytes(), equals([128])); - }); - - test('handle Uint16 maximum value (65535)', () { - writer.writeUint16(65535); - expect(writer.takeBytes(), equals([255, 255])); - }); - - test('handle Int16 maximum positive value (32767)', () { - writer.writeInt16(32767); - expect(writer.takeBytes(), equals([127, 255])); - }); - - test('handle Uint32 maximum value (4294967295)', () { - writer.writeUint32(4294967295); - expect(writer.takeBytes(), equals([255, 255, 255, 255])); - }); - - test('handle Int32 maximum positive value (2147483647)', () { - writer.writeInt32(2147483647); - expect(writer.takeBytes(), equals([127, 255, 255, 255])); - }); - - test('handle Uint64 maximum value (9223372036854775807)', () { - writer.writeUint64(9223372036854775807); - expect( - writer.takeBytes(), - equals([127, 255, 255, 255, 255, 255, 255, 255]), - ); - }); - - test( - 'should handle Int64 maximum positive value (9223372036854775807)', - () { - writer.writeInt64(9223372036854775807); - expect( - writer.takeBytes(), - equals([127, 255, 255, 255, 255, 255, 255, 255]), - ); - }, - ); - }); - - group('Boundary values - Minimum', () { - test('handle Uint8 minimum value (0)', () { - writer.writeUint8(0); - expect(writer.takeBytes(), equals([0])); - }); - - test('handle Int8 zero value', () { - writer.writeInt8(0); - expect(writer.takeBytes(), equals([0])); - }); - - test('handle Uint16 minimum value (0)', () { - writer.writeUint16(0); - expect(writer.takeBytes(), equals([0, 0])); - }); - - test('handle Int16 zero value', () { - writer.writeInt16(0); - expect(writer.takeBytes(), equals([0, 0])); - }); - - test('handle Uint32 minimum value (0)', () { - writer.writeUint32(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0])); - }); - - test('handle Int32 zero value', () { - writer.writeInt32(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0])); - }); - - test('handle Uint64 minimum value (0)', () { - writer.writeUint64(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); - }); - - test('handle Int64 zero value', () { - writer.writeInt64(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); - }); - }); - - group('Multiple operations', () { - test('handle multiple consecutive takeBytes calls', () { - writer.writeUint8(1); - expect(writer.takeBytes(), equals([1])); - - writer.writeUint8(2); - expect(writer.takeBytes(), equals([2])); - - writer.writeUint8(3); - expect(writer.takeBytes(), equals([3])); - }); - - test('handle toBytes followed by reset', () { - writer - ..writeUint8(42) - ..writeUint8(100); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([42, 100])); - - writer.reset(); - expect(writer.toBytes(), isEmpty); - expect(writer.bytesWritten, equals(0)); - }); - - test('handle multiple toBytes calls without modification', () { - writer - ..writeUint8(1) - ..writeUint8(2); - - final bytes1 = writer.toBytes(); - final bytes2 = writer.toBytes(); - final bytes3 = writer.toBytes(); - - expect(bytes1, equals([1, 2])); - expect(bytes2, equals([1, 2])); - expect(bytes3, equals([1, 2])); - }); - }); - - group('Byte array types', () { - test('accept Uint8List in writeBytes', () { - final data = Uint8List.fromList([1, 2, 3, 4, 5]); - writer.writeBytes(data); - expect(writer.takeBytes(), equals([1, 2, 3, 4, 5])); - }); - - test('accept regular List in writeBytes', () { - final data = [10, 20, 30, 40, 50]; - writer.writeBytes(data); - expect(writer.takeBytes(), equals([10, 20, 30, 40, 50])); - }); - - test('handle mixed types in sequence', () { - writer - ..writeBytes(Uint8List.fromList([1, 2])) - ..writeBytes([3, 4]) - ..writeUint8(5); - - expect(writer.takeBytes(), equals([1, 2, 3, 4, 5])); - }); - - test('writeBytes with offset parameter', () { - final data = [1, 2, 3, 4, 5]; - writer.writeBytes(data, 2); // Write from index 2: [3, 4, 5] - expect(writer.takeBytes(), equals([3, 4, 5])); - }); - - test('writeBytes with offset and length parameters', () { - final data = [1, 2, 3, 4, 5]; - writer.writeBytes(data, 1, 3); // Write [2, 3, 4] - expect(writer.takeBytes(), equals([2, 3, 4])); - }); - - test('writeBytes with offset at end', () { - final data = [1, 2, 3, 4, 5]; - writer.writeBytes(data, 5); // Write from end (empty) - expect(writer.takeBytes(), equals([])); - }); - - test('writeBytes with zero length', () { - final data = [1, 2, 3, 4, 5]; - writer.writeBytes(data, 0, 0); // Write 0 bytes - expect(writer.takeBytes(), equals([])); - }); - - test('writeBytes throws on negative offset', () { - final data = [1, 2, 3, 4, 5]; - expect( - () => writer.writeBytes(data, -1), - throwsA(isA()), - ); - }); - - test('writeBytes throws on negative length', () { - final data = [1, 2, 3, 4, 5]; - expect( - () => writer.writeBytes(data, 0, -1), - throwsA(isA()), - ); - }); - - test('writeBytes throws when offset exceeds list length', () { - final data = [1, 2, 3]; - expect( - () => writer.writeBytes(data, 4), - throwsA(isA()), - ); - }); - - test('writeBytes throws when offset + length exceeds list', () { - final data = [1, 2, 3, 4, 5]; - expect( - // offset 2 + length 5 > list length 5 - () => writer.writeBytes(data, 2, 5), - throwsA(isA()), - ); - }); - }); - - group('Float precision', () { - test('handle Float32 minimum positive subnormal value', () { - const minFloat32 = 1.4e-45; // Approximate minimum positive Float32 - writer.writeFloat32(minFloat32); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final value = reader.readFloat32(); - expect(value, greaterThan(0)); - }); - - test('handle Float64 minimum positive subnormal value', () { - const minFloat64 = 5e-324; // Approximate minimum positive Float64 - writer.writeFloat64(minFloat64); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final value = reader.readFloat64(); - expect(value, greaterThan(0)); - }); - - test('handle Float32 maximum value', () { - const maxFloat32 = 3.4028235e38; // Approximate maximum Float32 - writer.writeFloat32(maxFloat32); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), closeTo(maxFloat32, maxFloat32 * 0.01)); - }); - - test('handle Float64 maximum value', () { - const maxFloat64 = 1.7976931348623157e308; // Maximum Float64 - writer.writeFloat64(maxFloat64); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(maxFloat64)); - }); - }); - - group('UTF-8 encoding', () { - test('encode ASCII characters correctly', () { - writer.writeString('ABC123'); - expect(writer.takeBytes(), equals([65, 66, 67, 49, 50, 51])); - }); - - test('encode Cyrillic characters correctly', () { - writer.writeString('Привет'); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals('Привет')); - }); - - test('encode Chinese characters correctly', () { - const str = '你好世界'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('encode mixed Unicode string correctly', () { - const str = 'Hello мир 世界 🌍'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - }); - - group('Buffer growth strategy', () { - test('use 1.5x growth strategy', () { - final writer = BinaryWriter(initialBufferSize: 4) - // Fill initial 4 bytes - ..writeUint32(0); - expect(writer.bytesWritten, equals(4)); - - // Trigger expansion by writing one more byte - writer.writeUint8(1); - expect(writer.bytesWritten, equals(5)); - - // Should be able to write more without issues - writer - ..writeUint8(2) - ..writeUint8(3); - expect(writer.bytesWritten, equals(7)); - }); - - test( - 'should grow buffer to exact required size when 1.5x is insufficient', - () { - final writer = BinaryWriter(initialBufferSize: 4); - - // Write a large block that requires more than 1.5x growth - final largeData = Uint8List(100); - writer.writeBytes(largeData); - - expect(writer.bytesWritten, equals(100)); - }, - ); - }); - - group('State preservation', () { - test('preserve written data across toBytes calls', () { - writer.writeUint32(0x12345678); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([0x12, 0x34, 0x56, 0x78])); - - // Write more data - writer.writeUint32(0xABCDEF00); - - final bytes2 = writer.toBytes(); - expect( - bytes2, - equals([0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x00]), - ); - }); - - test( - 'should not affect data when calling bytesWritten multiple times', - () { - writer - ..writeUint8(1) - ..writeUint8(2) - ..writeUint8(3); - - expect(writer.bytesWritten, equals(3)); - expect(writer.bytesWritten, equals(3)); - expect(writer.bytesWritten, equals(3)); - - expect(writer.toBytes(), equals([1, 2, 3])); - }, - ); - }); - - group('Lone surrogate pairs', () { - test( - 'writeString handles lone high surrogate with allowMalformed=true', - () { - const testStr = 'Before\uD800After'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length, allowMalformed: true); - expect(result, isNotEmpty); - expect(result, contains('Before')); - expect(result, contains('After')); - expect(result.contains('\uFFFD') || result.contains('�'), isTrue); - }, - ); - - test( - 'writeString throws on lone high surrogate with allowMalformed=false', - () { - const testStr = 'Before\uD800After'; - expect( - () => writer.writeString(testStr, allowMalformed: false), - throwsA(isA()), - ); - }, - ); - - test( - 'writeString handles lone low surrogate with allowMalformed=true', - () { - const testStr = 'Before\uDC00After'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length, allowMalformed: true); - expect(result, isNotEmpty); - expect(result, contains('Before')); - expect(result, contains('After')); - expect(result.contains('\uFFFD') || result.contains('�'), isTrue); - }, - ); - - test( - 'writeString throws on lone low surrogate with allowMalformed=false', - () { - const testStr = 'Before\uDC00After'; - expect( - () => writer.writeString(testStr, allowMalformed: false), - throwsA(isA()), - ); - }, - ); - - test('writeString handles valid surrogate pair', () { - const testStr = 'Test\u{1F600}End'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - expect(result, equals(testStr)); - }); - - test('writeString handles mixed valid and invalid surrogates', () { - const testStr = 'A\u{1F600}B\uD800C'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length, allowMalformed: true); - expect(result, contains('A')); - expect(result, contains('B')); - expect(result, contains('C')); - expect(result.contains('\uFFFD') || result.contains('�'), isTrue); - }); - - test( - 'writeString throws on mixed surrogates with allowMalformed=false', - () { - const testStr = 'A\u{1F600}B\uD800C'; - expect( - () => writer.writeString(testStr, allowMalformed: false), - throwsA(isA()), - ); - }, - ); - }); - - group('Very large strings', () { - test('writeString with string exceeding initial buffer size', () { - final writer = BinaryWriter(initialBufferSize: 8); - const largeString = - 'This is a very long string that exceeds initial' - ' buffer size and should trigger buffer expansion properly'; - - writer.writeString(largeString); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - expect(result, equals(largeString)); - }); - - test('writeString with string requiring more than 1.5x growth', () { - final writer = BinaryWriter(initialBufferSize: 4); - const str = 'Very long string to force larger growth'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - expect(result, equals(str)); - }); - - test('writeString with multi-byte UTF-8 characters exceeding buffer', () { - final writer = BinaryWriter(initialBufferSize: 8); - const str = 'Привет мир! Это длинная строка для теста'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - expect(result, equals(str)); - }); - - test('writeString with Chinese characters requiring buffer growth', () { - final writer = BinaryWriter(initialBufferSize: 16); - const str = '这是一个非常长的中文字符串用于测试缓冲区扩展功能是否正常工作'; - - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final result = reader.readString(bytes.length); - expect(result, equals(str)); - }); - }); - - group('Uint64 maximum values', () { - test('writeUint64 with maximum safe integer', () { - const maxSafeInt = 9223372036854775807; - writer.writeUint64(maxSafeInt); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint64(), equals(maxSafeInt)); - }); - - test('writeUint64 with value 0', () { - writer.writeUint64(0); - final bytes = writer.takeBytes(); - expect(bytes, equals([0, 0, 0, 0, 0, 0, 0, 0])); - }); - - test('writeUint64 with large value in little-endian', () { - const largeValue = 123456789012345; // Safe for JS: < 2^53 - writer.writeUint64(largeValue, .little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint64(.little), equals(largeValue)); - }); - }); - - group('Buffer growth advanced', () { - test('exact buffer capacity boundary', () { - final writer = BinaryWriter(initialBufferSize: 8)..writeUint64(12345); - expect(writer.bytesWritten, equals(8)); - - writer.writeUint8(1); - expect(writer.bytesWritten, equals(9)); - - final bytes = writer.takeBytes(); - expect(bytes.length, equals(9)); - }); - - test('multiple expansions in sequence', () { - final writer = BinaryWriter(initialBufferSize: 4) - ..writeUint32(0x12345678); - expect(writer.bytesWritten, equals(4)); - - writer.writeUint8(0xAB); - expect(writer.bytesWritten, equals(5)); - - for (var i = 0; i < 20; i++) { - writer.writeUint8(i); - } - - expect(writer.bytesWritten, equals(25)); - }); - - test('large single write triggering immediate large expansion', () { - final writer = BinaryWriter(initialBufferSize: 8); - final largeData = Uint8List(1000); - for (var i = 0; i < 1000; i++) { - largeData[i] = i % 256; - } - - writer.writeBytes(largeData); - expect(writer.bytesWritten, equals(1000)); - - final bytes = writer.takeBytes(); - expect(bytes, equals(largeData)); - }); - - test('alternating small and large writes', () { - final writer = BinaryWriter(initialBufferSize: 16) - ..writeUint8(1) - ..writeBytes(Uint8List(100)) - ..writeUint8(2) - ..writeBytes(Uint8List(50)) - ..writeUint8(3); - - expect(writer.bytesWritten, equals(153)); - }); - }); - - group('Thread-safety verification', () { - test('float conversion uses instance buffers', () { - final writer1 = BinaryWriter(); - final writer2 = BinaryWriter(); - - writer1.writeFloat32(1.23); - writer2.writeFloat32(4.56); - - final bytes1 = writer1.takeBytes(); - final bytes2 = writer2.takeBytes(); - - final reader1 = BinaryReader(bytes1); - final reader2 = BinaryReader(bytes2); - - expect(reader1.readFloat32(), closeTo(1.23, 0.01)); - expect(reader2.readFloat32(), closeTo(4.56, 0.01)); - }); - - test('concurrent writers produce independent results', () { - final writer1 = BinaryWriter(); - final writer2 = BinaryWriter(); - - writer1.writeUint32(0x11111111); - writer2.writeUint32(0x22222222); - writer1.writeFloat64(3.14159); - writer2.writeFloat64(2.71828); - - final bytes1 = writer1.takeBytes(); - final bytes2 = writer2.takeBytes(); - - expect(bytes1.length, equals(12)); - expect(bytes2.length, equals(12)); - - final reader1 = BinaryReader(bytes1); - final reader2 = BinaryReader(bytes2); - - expect(reader1.readUint32(), equals(0x11111111)); - expect(reader2.readUint32(), equals(0x22222222)); - expect(reader1.readFloat64(), closeTo(3.14159, 0.00001)); - expect(reader2.readFloat64(), closeTo(2.71828, 0.00001)); - }); - }); - - group('State preservation advanced', () { - test('toBytes does not affect subsequent writes', () { - writer.writeUint32(0x12345678); - final snapshot1 = writer.toBytes(); - - writer.writeUint32(0xABCDEF00); - final snapshot2 = writer.toBytes(); - - expect(snapshot1.length, equals(4)); - expect(snapshot2.length, equals(8)); - - final reader1 = BinaryReader(snapshot1); - final reader2 = BinaryReader(snapshot2); - - expect(reader1.readUint32(), equals(0x12345678)); - expect(reader2.readUint32(), equals(0x12345678)); - expect(reader2.readUint32(), equals(0xABCDEF00)); - }); - - test('multiple toBytes calls return equivalent data', () { - writer - ..writeUint16(100) - ..writeUint16(200) - ..writeUint16(300); - - final snap1 = writer.toBytes(); - final snap2 = writer.toBytes(); - final snap3 = writer.toBytes(); - - expect(snap1, equals(snap2)); - expect(snap2, equals(snap3)); - }); - - test('reset after toBytes properly clears buffer', () { - writer - ..writeUint64(1234567890123456) // Safe for JS: < 2^53 - ..toBytes() - ..reset(); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - - writer.writeUint8(42); - expect(writer.toBytes(), equals([42])); - }); - }); - - group('Complex integration scenarios', () { - test('full write-read cycle with all types and mixed endianness', () { - writer - ..writeUint8(255) - ..writeInt8(-128) - ..writeUint16(65535) - ..writeInt16(-32768, .little) - ..writeUint32(4294967295, .little) - ..writeInt32(-2147483648) - ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808, .little) - ..writeFloat32(3.14159, .little) - ..writeFloat64(2.718281828) - ..writeString('Hello, 世界! 🌍') - ..writeBytes([1, 2, 3, 4, 5]); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(255)); - expect(reader.readInt8(), equals(-128)); - expect(reader.readUint16(), equals(65535)); - expect(reader.readInt16(.little), equals(-32768)); - expect(reader.readUint32(.little), equals(4294967295)); - expect(reader.readInt32(), equals(-2147483648)); - expect(reader.readUint64(), equals(9223372036854775807)); - expect(reader.readInt64(.little), equals(-9223372036854775808)); - expect(reader.readFloat32(.little), closeTo(3.14159, 0.00001)); - expect(reader.readFloat64(), closeTo(2.718281828, 0.000000001)); - - reader.skip(reader.availableBytes - 5); - expect(reader.readBytes(5), equals([1, 2, 3, 4, 5])); - }); - - test('writer reuse with takeBytes between operations', () { - writer - ..writeUint32(100) - ..writeString('First'); - final bytes1 = writer.takeBytes(); - - writer - ..writeUint32(200) - ..writeString('Second'); - final bytes2 = writer.takeBytes(); - - writer - ..writeUint32(300) - ..writeString('Third'); - final bytes3 = writer.takeBytes(); - - var reader = BinaryReader(bytes1); - expect(reader.readUint32(), equals(100)); - - reader = BinaryReader(bytes2); - expect(reader.readUint32(), equals(200)); - - reader = BinaryReader(bytes3); - expect(reader.readUint32(), equals(300)); - }); - - test('large mixed data write with buffer expansions', () { - final writer = BinaryWriter(initialBufferSize: 32); - - for (var i = 0; i < 100; i++) { - writer - ..writeUint8(i % 256) - ..writeUint16(i * 2) - ..writeUint32(i * 1000) - ..writeFloat32(i * 1.5); - } - - writer.writeString('Final string at the end'); - - final bytes = writer.takeBytes(); - expect(bytes.length, greaterThan(32)); - expect(bytes.length, greaterThan(1000)); - - final reader = BinaryReader(bytes); - expect(reader.readUint8(), equals(0)); - expect(reader.readUint16(), equals(0)); - expect(reader.readUint32(), equals(0)); - expect(reader.readFloat32(), closeTo(0, 0.01)); - }); - }); - - group('Memory efficiency', () { - test('takeBytes creates view not copy', () { - writer.writeUint32(0x12345678); - final bytes = writer.takeBytes(); - - expect(bytes, isA()); - expect(bytes.length, equals(4)); - }); - - test('toBytes creates view not copy', () { - writer.writeUint64(9876543210123); // Safe for JS: < 2^53 - final bytes = writer.toBytes(); - - expect(bytes, isA()); - expect(bytes.length, equals(8)); - }); - - test('buffer only grows when necessary', () { - final writer = BinaryWriter(initialBufferSize: 100); - - for (var i = 0; i < 50; i++) { - writer.writeUint8(i); - } - - expect(writer.bytesWritten, equals(50)); - final bytes = writer.toBytes(); - expect(bytes.length, equals(50)); - }); - }); - - group('VarBytes operations', () { - test('writeVarBytes with empty array', () { - final writer = BinaryWriter()..writeVarBytes([]); - final bytes = writer.takeBytes(); - - expect(bytes, equals([0])); // Just length 0 - }); - - test('writeVarBytes with small array', () { - final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(4)); // VarUint length - expect(bytes.sublist(1), equals([1, 2, 3, 4])); - }); - - test('writeVarBytes with 127 bytes (single-byte VarUint)', () { - final writer = BinaryWriter(); - final data = List.generate(127, (i) => i); - writer.writeVarBytes(data); - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(127)); // Single-byte VarUint - expect(bytes.length, equals(128)); // 1 (length) + 127 (data) - }); - - test('writeVarBytes with 128 bytes (two-byte VarUint)', () { - final writer = BinaryWriter(); - final data = List.generate(128, (i) => i & 0xFF); - writer.writeVarBytes(data); - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(0x80)); // First byte of VarUint 128 - expect(bytes[1], equals(0x01)); // Second byte of VarUint 128 - expect(bytes.length, equals(130)); // 2 (length) + 128 (data) - }); - - test('writeVarBytes with large array', () { - final writer = BinaryWriter(); - final data = List.generate(1000, (i) => (i * 7) & 0xFF); - writer.writeVarBytes(data); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - final length = reader.readVarUint(); - expect(length, equals(1000)); - - final readData = reader.readBytes(1000); - expect(readData, equals(data)); - }); - - test('writeVarBytes multiple arrays', () { - final writer = BinaryWriter() - ..writeVarBytes([1, 2]) - ..writeVarBytes([3, 4, 5]) - ..writeVarBytes([6]); - - final reader = BinaryReader(writer.toBytes()); - expect(reader.readVarBytes(), equals([1, 2])); - expect(reader.readVarBytes(), equals([3, 4, 5])); - expect(reader.readVarBytes(), equals([6])); - }); - }); - - group('VarString operations', () { - test('writeVarString with ASCII string', () { - final writer = BinaryWriter()..writeVarString('Hello'); - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(5)); // VarUint length - expect(bytes.sublist(1), equals([72, 101, 108, 108, 111])); // 'Hello' - }); - - test('writeVarString with UTF-8 multi-byte characters', () { - final writer = BinaryWriter() - ..writeVarString('世界'); // 2 characters, 6 bytes in UTF-8 - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(6)); // VarUint length (6 bytes) - expect(bytes.length, equals(7)); // 1 (length) + 6 (data) - }); - - test('writeVarString with emoji', () { - final writer = BinaryWriter() - ..writeVarString('🌍'); // 1 character, 4 bytes in UTF-8 - final bytes = writer.takeBytes(); - - expect(bytes[0], equals(4)); // VarUint length - expect(bytes.length, equals(5)); // 1 (length) + 4 (data) - }); - - test('writeVarString with empty string', () { - final writer = BinaryWriter()..writeVarString(''); - final bytes = writer.takeBytes(); - - expect(bytes, equals([0])); // Just length 0 - }); - - test('writeVarString with mixed content', () { - final writer = BinaryWriter()..writeVarString('Hi 世界 🌍!'); - final bytes = writer.takeBytes(); - - // 'Hi ' = 3, '世界' = 6, ' ' = 1, '🌍' = 4, '!' = 1 => 15 bytes - expect(bytes[0], equals(15)); // VarUint length - expect(bytes.length, equals(16)); // 1 + 15 - }); - - test('writeVarString with malformed handling', () { - final writer = BinaryWriter(); - // Lone high surrogate (U+D800) - final malformed = String.fromCharCode(0xD800); - - // Default allowMalformed=true should handle it - expect( - () => writer.writeVarString(malformed), - returnsNormally, - ); - }); - }); - - group('getUtf8Length function', () { - test('with ASCII only', () { - expect(getUtf8Length('Hello'), equals(5)); - expect(getUtf8Length('ABCDEFGH'), equals(8)); // Fast path - }); - - test('with empty string', () { - expect(getUtf8Length(''), equals(0)); - }); - - test('with 2-byte UTF-8 chars', () { - expect(getUtf8Length('café'), equals(5)); // 'caf' = 3, 'é' = 2 - expect(getUtf8Length('Привет'), equals(12)); // Each Cyrillic = 2 bytes - }); - - test('with 3-byte UTF-8 chars', () { - expect(getUtf8Length('世界'), equals(6)); // Each Chinese = 3 bytes - expect(getUtf8Length('你好'), equals(6)); - }); - - test('with 4-byte UTF-8 chars (emoji)', () { - expect(getUtf8Length('🌍'), equals(4)); - expect(getUtf8Length('🎉'), equals(4)); - expect(getUtf8Length('😀'), equals(4)); - }); - - test('with mixed content', () { - // 'Hello' = 5, ', ' = 2, '世界' = 6, '! ' = 2, '🌍' = 4 - expect(getUtf8Length('Hello, 世界! 🌍'), equals(19)); - }); - - test('matches actual UTF-8 encoding', () { - final strings = [ - 'Test', - 'Тест', - '测试', - '🧪', - 'Mix テスト 123', - 'A' * 100, // Long ASCII for fast path - ]; - - for (final str in strings) { - final calculated = getUtf8Length(str); - final actual = utf8.encode(str).length; - expect( - calculated, - equals(actual), - reason: 'Failed for string: "$str"', - ); - } - }); - - test('with surrogate pairs', () { - // Valid surrogate pair forms emoji - final emoji = String.fromCharCodes([0xD83C, 0xDF0D]); // 🌍 - expect(getUtf8Length(emoji), equals(4)); - }); - - test('with malformed high surrogate', () { - // High surrogate (0xD800-0xDBFF) not followed by low surrogate - // This triggers the malformed surrogate pair path in getUtf8Length - final malformed = String.fromCharCodes([ - 0xD800, - 0x0041, - ]); // High surrogate + 'A' - expect( - getUtf8Length(malformed), - equals(4), - ); // 3 bytes (replacement) + 1 byte (A) - }); - - test('with lone high surrogate at end', () { - // High surrogate at the end of string (also malformed) - final malformed = String.fromCharCodes([ - 0x0041, - 0xD800, - ]); // 'A' + high surrogate - expect( - getUtf8Length(malformed), - equals(4), - ); // 1 byte (A) + 3 bytes (replacement) - }); - }); - - group('Special UTF-8 cases', () { - test('writeString with only ASCII (fast path)', () { - const str = 'OnlyASCII123'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - expect(bytes.length, equals(str.length)); - }); - - test('writeString with mixed ASCII and multi-byte', () { - const str = 'ASCII_Юникод_中文'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - expect(bytes.length, greaterThan(str.length)); - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('writeString with only 4-byte characters (emojis)', () { - const str = '🚀🌟💻🎉🔥'; - writer.writeString(str); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(str)); - }); - - test('writeString empty string after previous writes', () { - writer - ..writeUint8(42) - ..writeString('') - ..writeUint8(43); - - final bytes = writer.takeBytes(); - expect(bytes, equals([42, 43])); - }); - }); - - group('writeBool', () { - test('writes true as 0x01', () { - writer.writeBool(true); - expect(writer.takeBytes(), equals([0x01])); - }); - - test('writes false as 0x00', () { - writer.writeBool(false); - expect(writer.takeBytes(), equals([0x00])); - }); - - test('writes multiple boolean values correctly', () { - writer - ..writeBool(true) - ..writeBool(false) - ..writeBool(true) - ..writeBool(true) - ..writeBool(false); - - expect(writer.takeBytes(), equals([0x01, 0x00, 0x01, 0x01, 0x00])); - }); - - test('can be read back with readBool', () { - writer - ..writeBool(true) - ..writeBool(false) - ..writeBool(true); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readBool(), isTrue); - expect(reader.readBool(), isFalse); - expect(reader.readBool(), isTrue); - }); - - test('updates bytesWritten correctly', () { - expect(writer.bytesWritten, equals(0)); - - writer.writeBool(true); - expect(writer.bytesWritten, equals(1)); - - writer.writeBool(false); - expect(writer.bytesWritten, equals(2)); - - writer.writeBool(true); - expect(writer.bytesWritten, equals(3)); - }); - - test('can be mixed with other write operations', () { - writer - ..writeUint8(42) - ..writeBool(true) - ..writeUint16(1000) - ..writeBool(false) - ..writeInt32(-500); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(42)); - expect(reader.readBool(), isTrue); - expect(reader.readUint16(), equals(1000)); - expect(reader.readBool(), isFalse); - expect(reader.readInt32(), equals(-500)); - }); - - test('expands buffer when needed', () { - // Write many booleans to trigger buffer expansion - for (var i = 0; i < 200; i++) { - writer.writeBool(i.isEven); - } - - final bytes = writer.takeBytes(); - expect(bytes.length, equals(200)); - - final reader = BinaryReader(bytes); - for (var i = 0; i < 200; i++) { - expect(reader.readBool(), equals(i.isEven)); - } - }); - - test('resets correctly after takeBytes', () { - writer - ..writeBool(true) - ..takeBytes() - ..writeBool(false); - expect(writer.takeBytes(), equals([0x00])); - }); - - test('works correctly with toBytes', () { - writer.writeBool(true); - final snapshot1 = writer.toBytes(); - expect(snapshot1, equals([0x01])); - - writer.writeBool(false); - final snapshot2 = writer.toBytes(); - expect(snapshot2, equals([0x01, 0x00])); - }); - - test('works correctly with reset', () { - writer - ..writeBool(true) - ..writeBool(false) - ..reset() - ..writeBool(false) - ..writeBool(true); - - expect(writer.toBytes(), equals([0x00, 0x01])); - }); - }); - }); - - group('BinaryWriterPool', () { - setUp(BinaryWriterPool.clear); - - tearDown(BinaryWriterPool.clear); - - test('acquire returns a working writer', () { - final writer = BinaryWriterPool.acquire()..writeUint32(42); - final bytes = writer.toBytes(); - BinaryWriterPool.release(writer); - - expect(bytes, hasLength(4)); - }); - - test('acquire creates new writer when pool is empty', () { - expect(BinaryWriterPool.stats.pooled, equals(0)); - - final writer = BinaryWriterPool.acquire(); - expect(writer, isNotNull); - BinaryWriterPool.release(writer); - }); - - test('release returns writer to pool', () { - final writer = BinaryWriterPool.acquire() - //Write some data to ensure buffer is used - ..writeUint32(42); - BinaryWriterPool.release(writer); - - final stats = BinaryWriterPool.stats; - expect(stats.pooled, equals(1)); - }); - - test('acquire reuses pooled writer', () { - final writer1 = BinaryWriterPool.acquire() - // Write some data to ensure buffer is used - ..writeUint32(42); - - BinaryWriterPool.release(writer1); - - expect(BinaryWriterPool.stats.pooled, equals(1)); - - final writer2 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.pooled, equals(0)); - - // Writer should be cleared - expect(writer2.bytesWritten, equals(0)); - - BinaryWriterPool.release(writer2); - }); - - test('released writer is reset', () { - final writer = BinaryWriterPool.acquire() - ..writeUint32(42) - ..writeString('Hello'); - BinaryWriterPool.release(writer); - - final reusedWriter = BinaryWriterPool.acquire(); - expect(reusedWriter.bytesWritten, equals(0)); - - reusedWriter.writeUint8(1); - final bytes = reusedWriter.toBytes(); - expect(bytes, equals([1])); - - BinaryWriterPool.release(reusedWriter); - }); - - test('clear empties the pool', () { - final writer1 = BinaryWriterPool.acquire(); - final writer2 = BinaryWriterPool.acquire(); - final writer3 = BinaryWriterPool.acquire(); - - BinaryWriterPool.release(writer1); - BinaryWriterPool.release(writer2); - BinaryWriterPool.release(writer3); - - expect(BinaryWriterPool.stats.pooled, equals(3)); - - BinaryWriterPool.clear(); - expect(BinaryWriterPool.stats.pooled, equals(0)); - }); - - test('getStatistics returns correct information', () { - final stats = BinaryWriterPool.stats; - - expect(stats.pooled, equals(0)); - expect(stats.maxPoolSize, equals(32)); - expect(stats.initialBufferSizer, equals(1024)); - expect(stats.maxReusableCapacity, equals(64 * 1024)); - }); - - test('acquireHit increments on pool reuse', () { - expect(BinaryWriterPool.stats.acquireHit, equals(0)); - - // First acquire is a miss - final writer1 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireHit, equals(0)); - expect(BinaryWriterPool.stats.acquireMiss, equals(1)); - - BinaryWriterPool.release(writer1); - - // Second acquire is a hit (reuses pooled writer) - final writer2 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireHit, equals(1)); - expect(BinaryWriterPool.stats.acquireMiss, equals(1)); - - BinaryWriterPool.release(writer2); - }); - - test('acquireMiss increments on new allocation', () { - expect(BinaryWriterPool.stats.acquireMiss, equals(0)); - - // Pool is empty, should create new writers - final writer1 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireMiss, equals(1)); - - final writer2 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireMiss, equals(2)); - - final writer3 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireMiss, equals(3)); - - BinaryWriterPool.release(writer1); - BinaryWriterPool.release(writer2); - BinaryWriterPool.release(writer3); - - // Now pool has 3 writers, no new allocations needed - final writer4 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.acquireMiss, equals(3)); - expect(BinaryWriterPool.stats.acquireHit, equals(1)); - - BinaryWriterPool.release(writer4); - }); - - test('peakPoolSize tracks maximum pool size', () { - expect(BinaryWriterPool.stats.peakPoolSize, equals(0)); - - // Create 3 writers simultaneously (all will be misses) - final writer1 = BinaryWriterPool.acquire(); - final writer2 = BinaryWriterPool.acquire(); - final writer3 = BinaryWriterPool.acquire(); - - // Release all 3 - pool size will grow to 3 - BinaryWriterPool.release(writer1); - expect(BinaryWriterPool.stats.pooled, equals(1)); - expect(BinaryWriterPool.stats.peakPoolSize, equals(1)); - - BinaryWriterPool.release(writer2); - expect(BinaryWriterPool.stats.pooled, equals(2)); - expect(BinaryWriterPool.stats.peakPoolSize, equals(2)); - - BinaryWriterPool.release(writer3); - expect(BinaryWriterPool.stats.pooled, equals(3)); - expect(BinaryWriterPool.stats.peakPoolSize, equals(3)); - - // Acquire one (pool size decreases but peak stays) - final writer4 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.pooled, equals(2)); - expect(BinaryWriterPool.stats.peakPoolSize, equals(3)); - - BinaryWriterPool.release(writer4); - }); - - test('discardedLargeBuffers increments when buffer exceeds limit', () { - expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); - - final writer = BinaryWriterPool.acquire(); - - // Write enough data to expand buffer beyond 64 KiB - final largeData = List.filled(70 * 1024, 42); - writer.writeBytes(largeData); - - BinaryWriterPool.release(writer); - - // Buffer should be discarded - expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(1)); - expect(BinaryWriterPool.stats.pooled, equals(0)); - - // Create another large buffer - final writer2 = BinaryWriterPool.acquire() - ..writeBytes(List.filled(100 * 1024, 1)); - BinaryWriterPool.release(writer2); - - expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(2)); - }); - - test('discardedLargeBuffers does not increment when pool is full', () { - expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); - - // Create 33 writers and release them all at once - // The pool can only hold 32, so the 33rd should be discarded - final writers = []; - for (var i = 0; i < 33; i++) { - writers.add(BinaryWriterPool.acquire()..writeUint32(i)); - } - for (final writer in writers) { - BinaryWriterPool.release(writer); - } - - expect(BinaryWriterPool.stats.pooled, equals(32)); - // discardedLargeBuffers only counts large buffers, not pool-full discards - expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); - }); - - test('discardedPoolFull increments when pool is full', () { - expect(BinaryWriterPool.stats.discardedPoolFull, equals(0)); - - // Create 33 writers and release them all at once - // The pool can only hold 32, so the 33rd should be discarded - final writers = []; - for (var i = 0; i < 33; i++) { - writers.add(BinaryWriterPool.acquire()..writeUint32(i)); - } - for (final writer in writers) { - BinaryWriterPool.release(writer); - } - - expect(BinaryWriterPool.stats.pooled, equals(32)); - expect(BinaryWriterPool.stats.discardedPoolFull, equals(1)); - }); - - test('takeBytes() then release() works correctly', () { - final writer = BinaryWriterPool.acquire()..writeUint32(42); - final bytes = writer.takeBytes(); - expect(bytes.length, equals(4)); - expect(BinaryWriterPool.stats.pooled, equals(0)); - - BinaryWriterPool.release(writer); - expect(BinaryWriterPool.stats.pooled, equals(1)); - - final writer2 = BinaryWriterPool.acquire(); - expect(writer2, isNotNull); - BinaryWriterPool.release(writer2); - }); - - test('reset() then release() works correctly', () { - final writer = BinaryWriterPool.acquire() - ..writeUint32(42) - ..reset(); - expect(writer.bytesWritten, equals(0)); - expect(BinaryWriterPool.stats.pooled, equals(0)); - - BinaryWriterPool.release(writer); - expect(BinaryWriterPool.stats.pooled, equals(1)); - - final writer2 = BinaryWriterPool.acquire(); - expect(writer2, isNotNull); - BinaryWriterPool.release(writer2); - }); - - test('totalAcquires returns sum of hits and misses', () { - expect(BinaryWriterPool.stats.totalAcquires, equals(0)); - - final writer1 = BinaryWriterPool.acquire(); // miss - expect(BinaryWriterPool.stats.totalAcquires, equals(1)); - - BinaryWriterPool.release(writer1); - - final writer2 = BinaryWriterPool.acquire(); // hit - expect(BinaryWriterPool.stats.totalAcquires, equals(2)); - - final writer3 = BinaryWriterPool.acquire(); // miss - expect(BinaryWriterPool.stats.totalAcquires, equals(3)); - - expect(BinaryWriterPool.stats.acquireHit, equals(1)); - expect(BinaryWriterPool.stats.acquireMiss, equals(2)); - - BinaryWriterPool.release(writer2); - BinaryWriterPool.release(writer3); - }); - - test('hitRate returns correct percentage', () { - // Initially no acquires - expect(BinaryWriterPool.stats.hitRate, equals(0.0)); - - // First acquire is always a miss - final writer1 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.hitRate, equals(0.0)); // 0/1 = 0% - - BinaryWriterPool.release(writer1); - - // Second acquire is a hit - final writer2 = BinaryWriterPool.acquire(); - expect(BinaryWriterPool.stats.hitRate, equals(0.5)); // 1/2 = 50% - - BinaryWriterPool.release(writer2); - - // Third acquire is a hit - final writer3 = BinaryWriterPool.acquire(); - expect( - BinaryWriterPool.stats.hitRate, - closeTo(0.666, 0.001), - ); // 2/3 ≈ 66.7% - - BinaryWriterPool.release(writer3); - }); - - test('clear resets all statistics', () { - // Generate some activity - // Create 2 writers to have one in pool after operations - final writerA = BinaryWriterPool.acquire(); // miss - final writerB = BinaryWriterPool.acquire(); // miss - writerA.writeUint32(1); - BinaryWriterPool.release(writerA); // pool=1 - - // Reuse writerA - final writer2 = - BinaryWriterPool.acquire() // hit, pool=0 - ..writeUint32(2); - BinaryWriterPool.release(writer2); // pool=1 - - // Reuse again - final writer3 = - BinaryWriterPool.acquire() // hit, pool=0 - ..writeUint32(3); - BinaryWriterPool.release(writer3); // pool=1 - - // Now use writerB with large buffer - writerB.writeBytes(List.filled(70 * 1024, 1)); - BinaryWriterPool.release(writerB); // Discarded, pool stays =1 - - // Verify stats are non-zero - final stats = BinaryWriterPool.stats; - expect(stats.pooled, equals(1)); // writerA is still pooled - expect(stats.acquireHit, equals(2)); // writer2 and writer3 were hits - expect(stats.acquireMiss, equals(2)); // writerA and writerB were misses - expect( - stats.peakPoolSize, - equals(1), - ); // Never more than 1 in pool at once - expect(stats.discardedLargeBuffers, equals(1)); // writerB was discarded - - // Clear should reset everything - BinaryWriterPool.clear(); - - final clearedStats = BinaryWriterPool.stats; - expect(clearedStats.pooled, equals(0)); - expect(clearedStats.acquireHit, equals(0)); - expect(clearedStats.acquireMiss, equals(0)); - expect(clearedStats.peakPoolSize, equals(0)); - expect(clearedStats.discardedLargeBuffers, equals(0)); - expect(clearedStats.hitRate, equals(0.0)); - }); - - test('pool respects max pool size', () { - // Create and release more writers than the pool can hold - final writers = []; - for (var i = 0; i < 40; i++) { - writers.add(BinaryWriterPool.acquire()); - } - - writers.forEach(BinaryWriterPool.release); - - final stats = BinaryWriterPool.stats; - expect(stats.pooled, equals(32)); // Max pool size - }); - - test('writers with large buffers are not pooled', () { - final writer = BinaryWriterPool.acquire(); - - // Write enough data to expand buffer beyond 64 KiB - final largeData = List.filled(70 * 1024, 42); - writer.writeBytes(largeData); - - BinaryWriterPool.release(writer); - - // Writer should not be pooled due to large buffer - final stats = BinaryWriterPool.stats; - expect(stats.pooled, equals(0)); - }); - - test('double release is safe (ignored)', () { - final writer = BinaryWriterPool.acquire(); - BinaryWriterPool.release(writer); - expect(BinaryWriterPool.stats.pooled, equals(1)); - - // Second release should be ignored - BinaryWriterPool.release(writer); - expect(BinaryWriterPool.stats.pooled, equals(1)); - }); - - test('multiple writers work independently', () { - final writer1 = BinaryWriterPool.acquire(); - final writer2 = BinaryWriterPool.acquire(); - final writer3 = BinaryWriterPool.acquire(); - - writer1.writeUint32(100); - writer2.writeUint32(200); - writer3.writeUint32(300); - - final bytes1 = writer1.toBytes(); - final bytes2 = writer2.toBytes(); - final bytes3 = writer3.toBytes(); - - final reader1 = BinaryReader(bytes1); - final reader2 = BinaryReader(bytes2); - final reader3 = BinaryReader(bytes3); - - expect(reader1.readUint32(), equals(100)); - expect(reader2.readUint32(), equals(200)); - expect(reader3.readUint32(), equals(300)); - - BinaryWriterPool.release(writer1); - BinaryWriterPool.release(writer2); - BinaryWriterPool.release(writer3); - }); - - test('try-finally pattern works correctly', () { - late Uint8List bytes; - - final writer = BinaryWriterPool.acquire(); - try { - writer - ..writeUint32(42) - ..writeString('Test'); - bytes = writer.toBytes(); - } finally { - BinaryWriterPool.release(writer); - } - - expect(bytes, isNotNull); - expect(BinaryWriterPool.stats.pooled, equals(1)); - - final reader = BinaryReader(bytes); - expect(reader.readUint32(), equals(42)); - }); - - test('takeBytes and release work together', () { - final writer = BinaryWriterPool.acquire()..writeUint32(123); - final bytes = writer.takeBytes(); // This resets the writer - BinaryWriterPool.release(writer); - - expect(bytes, hasLength(4)); - expect(BinaryWriterPool.stats.pooled, equals(1)); - - // Verify writer was properly reset when returned - final reusedWriter = BinaryWriterPool.acquire(); - expect(reusedWriter.bytesWritten, equals(0)); - BinaryWriterPool.release(reusedWriter); - }); - - test('pool handles multiple acquire-release cycles', () { - for (var cycle = 0; cycle < 10; cycle++) { - final writer = BinaryWriterPool.acquire()..writeUint32(cycle); - - final bytes = writer.toBytes(); - final reader = BinaryReader(bytes); - expect(reader.readUint32(), equals(cycle)); - - BinaryWriterPool.release(writer); - } - - // Should have 1 writer in pool after all cycles - expect(BinaryWriterPool.stats.pooled, equals(1)); - }); - - test('writers can write complex data structures', () { - final writer = BinaryWriterPool.acquire(); - try { - // Write a complex structure - writer - ..writeVarUint(5) // Array length - ..writeString('Item1') - ..writeString('Item2') - ..writeString('Item3') - ..writeString('Привет') // Cyrillic - ..writeString('🌍'); // Emoji - - final bytes = writer.toBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(5)); - expect(reader.readString(5), equals('Item1')); - expect(reader.readString(5), equals('Item2')); - expect(reader.readString(5), equals('Item3')); - expect(reader.readString(12), equals('Привет')); - expect(reader.readString(4), equals('🌍')); - } finally { - BinaryWriterPool.release(writer); - } - }); - - test('pool statistics remain accurate during stress test', () { - // Acquire multiple writers - final writers = []; - for (var i = 0; i < 10; i++) { - writers.add(BinaryWriterPool.acquire()); - } - expect(BinaryWriterPool.stats.pooled, equals(0)); - - // Release half - for (var i = 0; i < 5; i++) { - BinaryWriterPool.release(writers[i]); - } - expect(BinaryWriterPool.stats.pooled, equals(5)); - - // Acquire some back - for (var i = 0; i < 3; i++) { - BinaryWriterPool.acquire(); - } - expect(BinaryWriterPool.stats.pooled, equals(2)); - - // Release remaining - for (var i = 5; i < 10; i++) { - BinaryWriterPool.release(writers[i]); - } - expect(BinaryWriterPool.stats.pooled, equals(7)); - }); - - test('default buffer size is appropriate for common use cases', () { - final writer = BinaryWriterPool.acquire(); - try { - // Write typical message - writer - ..writeUint32(12345) - ..writeString('Username') - ..writeFloat64(3.14159) - ..writeBool(true); - - expect(writer.bytesWritten, lessThan(1024)); // Default buffer size - } finally { - BinaryWriterPool.release(writer); - } - }); - - test('pool handles edge case of zero writes', () { - final writer = BinaryWriterPool.acquire(); - // Don't write anything - final bytes = writer.toBytes(); - BinaryWriterPool.release(writer); - - expect(bytes, isEmpty); - expect(BinaryWriterPool.stats.pooled, equals(1)); - }); - - test('pooled writer buffer capacity persists across reuse', () { - final writer1 = BinaryWriterPool.acquire(); - - // Expand buffer by writing data - final data = List.filled(2048, 42); - writer1.writeBytes(data); - - BinaryWriterPool.release(writer1); - - // Reuse the same writer - final writer2 = BinaryWriterPool.acquire() - // Writing smaller amount should not allocate new buffer - ..writeUint32(123); - expect(writer2.bytesWritten, equals(4)); - - BinaryWriterPool.release(writer2); - }); - }); - - group('VarInt/VarUint edge cases', () { - test('writeVarUint with maximum safe 64-bit value', () { - final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); - }); - - test('writeVarInt with maximum positive value', () { - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - // disabling lint for large integer literal - // ignore: avoid_js_rounded_ints - expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); - }); - - test('writeVarInt with minimum negative value', () { - final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readVarInt(), equals(-0x4000000000000000)); - }); - - test('writeVarUint boundary transitions', () { - final writer = BinaryWriter() - ..writeVarUint(0x7F) // Last 1-byte value - ..writeVarUint(0x80) // First 2-byte value - ..writeVarUint(0x3FFF) // Last 2-byte value - ..writeVarUint(0x4000) // First 3-byte value - ..writeVarUint(0x1FFFFF) // Last 3-byte value - ..writeVarUint(0x200000); // First 4-byte value - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarUint(), equals(0x7F)); - expect(reader.readVarUint(), equals(0x80)); - expect(reader.readVarUint(), equals(0x3FFF)); - expect(reader.readVarUint(), equals(0x4000)); - expect(reader.readVarUint(), equals(0x1FFFFF)); - expect(reader.readVarUint(), equals(0x200000)); - }); - - test('writeVarInt ZigZag boundary transitions', () { - final writer = BinaryWriter() - ..writeVarInt(-64) // Last 1-byte negative - ..writeVarInt(-65) // First 2-byte negative - ..writeVarInt(63) // Last 1-byte positive - ..writeVarInt(64); // First 2-byte positive - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarInt(), equals(-64)); - expect(reader.readVarInt(), equals(-65)); - expect(reader.readVarInt(), equals(63)); - expect(reader.readVarInt(), equals(64)); - }); - - test('writeVarUint with all 10-byte value (near maximum)', () { - // Maximum VarUint uses 9-10 bytes depending on value - const largeValue = 0x7FFFFFFFFFFFFFFF; - final writer = BinaryWriter()..writeVarUint(largeValue); - final bytes = writer.takeBytes(); - - expect(bytes.length, equals(9)); // This value encodes to 9 bytes - - final reader = BinaryReader(bytes); - expect(reader.readVarUint(), equals(largeValue)); - }); - }); - - group('VarBytes/VarString edge cases', () { - test('writeVarBytes with maximum single-byte length (127 bytes)', () { - final writer = BinaryWriter(); - final data = List.generate(127, (i) => i); - writer.writeVarBytes(data); - final bytes = writer.takeBytes(); - - // VarUint(127) = 1 byte + 127 data bytes = 128 total - expect(bytes.length, equals(128)); - expect(bytes[0], equals(127)); - - final reader = BinaryReader(bytes); - expect(reader.readVarBytes(), equals(data)); - }); - - test('writeVarBytes with minimum two-byte length (128 bytes)', () { - final writer = BinaryWriter(); - final data = List.generate(128, (i) => i & 0xFF); - writer.writeVarBytes(data); - final bytes = writer.takeBytes(); - - // VarUint(128) = 2 bytes + 128 data bytes = 130 total - expect(bytes.length, equals(130)); - - final reader = BinaryReader(bytes); - expect(reader.readVarBytes(), equals(data)); - }); - - test('writeVarString with ASCII at 127 character boundary', () { - final writer = BinaryWriter(); - final str = 'A' * 127; // 127 ASCII chars = 127 bytes - writer.writeVarString(str); - final bytes = writer.takeBytes(); - - // VarUint(127) = 1 byte + 127 bytes = 128 total - expect(bytes.length, equals(128)); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals(str)); - }); - - test('writeVarString with UTF-8 multi-byte at boundary', () { - final writer = BinaryWriter(); - // Each Cyrillic char = 2 bytes, 64 chars = 128 bytes - final str = 'Я' * 64; - writer.writeVarString(str); - final bytes = writer.takeBytes(); - - // VarUint(128) = 2 bytes + 128 bytes = 130 total - expect(bytes.length, equals(130)); - - final reader = BinaryReader(bytes); - expect(reader.readVarString(), equals(str)); - }); - - test('writeVarBytes triggers buffer expansion', () { - final writer = BinaryWriter(initialBufferSize: 16); - final largeData = List.generate(1000, (i) => i & 0xFF); - - writer.writeVarBytes(largeData); - - final bytes = writer.takeBytes(); - expect(bytes.length, greaterThan(1000)); - - final reader = BinaryReader(bytes); - expect(reader.readVarBytes(), equals(largeData)); - }); - }); - - group('Complex error scenarios', () { - test( - 'writeString with extremely long string triggers multiple expansions', - () { - final writer = BinaryWriter(initialBufferSize: 8); - final longString = 'A' * 10000; - - writer.writeString(longString); - final bytes = writer.takeBytes(); - - expect(bytes.length, equals(10000)); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals(longString)); - }, - ); - - test('alternating VarInt and fixed writes with buffer growth', () { - final writer = BinaryWriter(initialBufferSize: 16); - - for (var i = 0; i < 50; i++) { - writer - ..writeVarUint(i * 100) - ..writeUint32(i) - ..writeVarInt(-i); - } - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - for (var i = 0; i < 50; i++) { - expect(reader.readVarUint(), equals(i * 100)); - expect(reader.readUint32(), equals(i)); - expect(reader.readVarInt(), equals(-i)); - } - }); - - test('writeVarString with mixed malformed and valid UTF-8', () { - final writer = BinaryWriter() - // Valid string first - ..writeVarString('Valid'); - - // Malformed string with allowMalformed=true - const malformed = 'Test\uD800End'; - writer.writeVarString(malformed); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readVarString(), equals('Valid')); - final result = reader.readVarString(allowMalformed: true); - expect(result, contains('Test')); - expect(result, contains('End')); - }); - - test('complex interleaved writes maintain correct offsets', () { - final writer = BinaryWriter() - ..writeUint8(1) - ..writeVarUint(300) - ..writeUint16(1000) - ..writeVarInt(-500) - ..writeUint32(0xDEADBEEF) - ..writeVarString('Test') - ..writeBool(true) - ..writeVarBytes([1, 2, 3, 4, 5]) - ..writeFloat32(3.14) - ..writeUint64(123456789); - - final bytes = writer.takeBytes(); - final reader = BinaryReader(bytes); - - expect(reader.readUint8(), equals(1)); - expect(reader.readVarUint(), equals(300)); - expect(reader.readUint16(), equals(1000)); - expect(reader.readVarInt(), equals(-500)); - expect(reader.readUint32(), equals(0xDEADBEEF)); - expect(reader.readVarString(), equals('Test')); - expect(reader.readBool(), isTrue); - expect(reader.readVarBytes(), equals([1, 2, 3, 4, 5])); - expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readUint64(), equals(123456789)); - expect(reader.availableBytes, equals(0)); - }); - - group('Concise API', () { - test('call() is an alias for writeBytes', () { - final writer = BinaryWriter(); - writer([10, 20, 30]); - expect(writer.takeBytes(), equals([10, 20, 30])); - }); - }); - - group('Extra error handling', () { - test( - 'writeString handles high surrogate followed by non-low surrogate', - () { - final writer = BinaryWriter(); - const testStr = 'A\uD800B'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - expect(bytes.length, equals(5)); - expect(bytes[0], equals(0x41)); // 'A' - expect(bytes[1], equals(0xEF)); // U+FFFD start - expect(bytes[4], equals(0x42)); // 'B' - }, - ); - - test( - 'writeString handles lone high surrogate at end of string', - () { - final writer = BinaryWriter(); - const testStr = 'A\uD800'; - writer.writeString(testStr); - final bytes = writer.takeBytes(); - expect(bytes.length, equals(4)); - }, - ); - }); - - group('BinaryWriterPool Extra', () { - test('withWriter executes action and releases writer', () { - final result = BinaryWriterPool.withWriter((w) { - w.writeUint32(42); - return w.toBytes(); - }); - expect(result, equals([0, 0, 0, 42])); - expect(BinaryWriterPool.stats.pooled, greaterThan(0)); - }); - - test('withWriter releases writer even on error', () { - BinaryWriterPool.clear(); - expect( - () => BinaryWriterPool.withWriter((w) { - throw Exception('Test'); - }), - throwsException, - ); - expect(BinaryWriterPool.stats.pooled, equals(1)); - }); - - test('acquire expands pooled writer if requested size is larger', () { - BinaryWriterPool.clear(); - final writer = BinaryWriterPool.acquire(100); - BinaryWriterPool.release(writer); - - // Now acquire with larger buffer - final largerWriter = BinaryWriterPool.acquire(1000); - expect(largerWriter.capacity, greaterThanOrEqualTo(1000)); - BinaryWriterPool.release(largerWriter); - }); - }); - - group('Coverage edge cases', () { - test('writeVarString with 16KB+ string (2-byte VarInt)', () { - final writer = BinaryWriter(); - final longStr = 'a' * 20000; - writer.writeVarString(longStr); - final reader = BinaryReader(writer.takeBytes()); - expect(reader.readVarString(), equals(longStr)); - }); - - test('writeVarString with 2MB+ string (3-byte VarInt)', () { - final writer = BinaryWriter(); - // This is a large test, but necessary for coverage - final longStr = 'a' * (256 * 1024); - writer.writeVarString(longStr); - final reader = BinaryReader(writer.takeBytes()); - expect(reader.readVarString(), equals(longStr)); - }); - }); - }); -} diff --git a/test/unit/binary_writer_var_types_test.dart b/test/unit/binary_writer_var_types_test.dart new file mode 100644 index 0000000..780f1b4 --- /dev/null +++ b/test/unit/binary_writer_var_types_test.dart @@ -0,0 +1,144 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('BinaryWriter Variable-Length Types', () { + late BinaryWriter writer; + + setUp(() { + writer = BinaryWriter(); + }); + + group('VarInt and VarUint', () { + test('writes VarUint single byte (0)', () { + writer.writeVarUint(0); + expect(writer.takeBytes(), [0]); + }); + + test('writes VarUint single byte (127)', () { + writer.writeVarUint(127); + expect(writer.takeBytes(), [127]); + }); + + test('writes VarUint two bytes (128)', () { + writer.writeVarUint(128); + expect(writer.takeBytes(), [0x80, 0x01]); + }); + + test('writes VarUint three bytes (16384)', () { + writer.writeVarUint(16384); + expect(writer.takeBytes(), [0x80, 0x80, 0x01]); + }); + + test('writes VarUint large value', () { + writer.writeVarUint(1 << 30); + expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); + }); + + test('writes VarInt (ZigZag) encoding for positive value 1', () { + writer.writeVarInt(1); + expect(writer.takeBytes(), [2]); + }); + + test('writes VarInt (ZigZag) encoding for negative value -1', () { + writer.writeVarInt(-1); + expect(writer.takeBytes(), [1]); + }); + + test('writes VarInt ZigZag encoding for large values', () { + writer.writeVarInt(2147483647); + expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); + + writer + ..reset() + ..writeVarInt(-2147483648); + expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + }); + + test('writeVarUint boundary transitions', () { + writer + ..writeVarUint(0x7F) // Last 1-byte value + ..writeVarUint(0x80) // First 2-byte value + ..writeVarUint(0x3FFF) // Last 2-byte value + ..writeVarUint(0x4000); // First 3-byte value + + final reader = BinaryReader(writer.takeBytes()); + expect(reader.readVarUint(), equals(0x7F)); + expect(reader.readVarUint(), equals(0x80)); + expect(reader.readVarUint(), equals(0x3FFF)); + expect(reader.readVarUint(), equals(0x4000)); + }); + + test( + 'writeVarUint with negative value must not use fast path', + () { + writer.writeVarUint(-1); + final bytes = writer.takeBytes(); + expect(bytes.length, 10); + expect(bytes[0], 0xFF); + expect(bytes[9], 0x01); + }, + ); + }); + + group('VarBytes', () { + test('writeVarBytes with empty array', () { + writer.writeVarBytes([]); + expect(writer.takeBytes(), equals([0])); + }); + + test('writeVarBytes with small array', () { + writer.writeVarBytes([1, 2, 3, 4]); + final bytes = writer.takeBytes(); + expect(bytes[0], equals(4)); + expect(bytes.sublist(1), equals([1, 2, 3, 4])); + }); + + test('writeVarBytes with 128 bytes (two-byte VarUint length)', () { + final data = List.generate(128, (i) => i & 0xFF); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + expect(bytes[0], equals(0x80)); + expect(bytes[1], equals(0x01)); + expect(bytes.length, equals(130)); + }); + + test('writeVarBytes triggers buffer expansion', () { + final w = BinaryWriter(initialBufferSize: 16); + final largeData = List.generate(1000, (i) => i & 0xFF); + w.writeVarBytes(largeData); + final bytes = w.takeBytes(); + expect(bytes.length, greaterThan(1000)); + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), equals(largeData)); + }); + }); + + group('VarString', () { + test('writeVarString with ASCII string', () { + writer.writeVarString('Hello'); + final bytes = writer.takeBytes(); + expect(bytes[0], equals(5)); + expect(bytes.sublist(1), equals([72, 101, 108, 108, 111])); + }); + + test('writeVarString with UTF-8 multi-byte characters', () { + writer.writeVarString('世界'); + final bytes = writer.takeBytes(); + expect(bytes[0], equals(6)); + expect(bytes.length, equals(7)); + }); + + test('writeVarString with empty string', () { + writer.writeVarString(''); + expect(writer.takeBytes(), equals([0])); + }); + + test('writeVarString with malformed handling', () { + // Lone high surrogate + final malformed = String.fromCharCode(0xD800); + expect(() => writer.writeVarString(malformed), returnsNormally); + }); + }); + }); +} From f0bb7a0bc7f88e9cda0eb2e72d018a9dd1da8e68 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 13:05:26 +0300 Subject: [PATCH 05/17] wip: refactoring --- lib/pro_binary.dart | 2 + lib/src/stream/binary_stream_transformer.dart | 75 +++ lib/src/stream/stream_binary_reader.dart | 519 ++++++++++++++++++ pubspec.yaml | 2 +- .../binary_stream_transformer_test.dart | 53 ++ .../stream_binary_reader_nested_test.dart | 42 ++ test/stream/stream_binary_reader_test.dart | 79 +++ 7 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 lib/src/stream/binary_stream_transformer.dart create mode 100644 lib/src/stream/stream_binary_reader.dart create mode 100644 test/stream/binary_stream_transformer_test.dart create mode 100644 test/stream/stream_binary_reader_nested_test.dart create mode 100644 test/stream/stream_binary_reader_test.dart diff --git a/lib/pro_binary.dart b/lib/pro_binary.dart index ed0e0c1..de5dc6f 100644 --- a/lib/pro_binary.dart +++ b/lib/pro_binary.dart @@ -3,3 +3,5 @@ library; export 'src/binary_reader.dart'; export 'src/binary_writer.dart'; +export 'src/stream/binary_stream_transformer.dart'; +export 'src/stream/stream_binary_reader.dart'; diff --git a/lib/src/stream/binary_stream_transformer.dart b/lib/src/stream/binary_stream_transformer.dart new file mode 100644 index 0000000..9271d8c --- /dev/null +++ b/lib/src/stream/binary_stream_transformer.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'stream_binary_reader.dart'; + +/// A [StreamTransformer] that simplifies parsing binary messages from a stream. +/// +/// It manages an internal [StreamBinaryReader] and handles +/// [NotEnoughDataException] by automatically rolling back the reader state +/// and waiting for more data +/// from the stream. +/// +/// To use it, extend this class and implement the [parse] method. +/// +/// Example: +/// ```dart +/// class MyMessageTransformer extends BinaryStreamTransformer { +/// @override +/// MyMessage? parse(StreamBinaryReader reader) { +/// final id = reader.readUint32(); +/// final name = reader.readVarString(); +/// return MyMessage(id, name); +/// } +/// } +/// +/// // Usage: +/// stream.transform(MyMessageTransformer()).listen((msg) => print(msg.name)); +/// ``` +abstract class BinaryStreamTransformer + extends StreamTransformerBase, T> { + /// Creates a new [BinaryStreamTransformer]. + const BinaryStreamTransformer(); + + @override + Stream bind(Stream> stream) async* { + final reader = StreamBinaryReader(); + + await for (final chunk in stream) { + reader.addChunk(chunk); + yield* _parseLoop(reader); + } + + // Final attempt to parse remaining data after stream is closed + yield* _parseLoop(reader); + } + + Stream _parseLoop(StreamBinaryReader reader) async* { + while (reader.availableBytes > 0) { + reader.bookmark(); + try { + final result = parse(reader); + if (result == null) { + reader.rollback(); + break; // Wait for more data + } else { + reader.commit(); + yield result; + } + } on NotEnoughDataException { + reader.rollback(); + break; // Wait for more data + } catch (e) { + reader.rollback(); + rethrow; + } + } + } + + /// Parses a single message from the [reader]. + /// + /// Return the parsed object, or `null` if there is not enough data to + /// complete the message. You can also just read freely, and if a + /// [NotEnoughDataException] is thrown, the transformer will automatically + /// catch it, rollback the reader's state, and wait for more data. + T? parse(StreamBinaryReader reader); +} diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart new file mode 100644 index 0000000..298afbf --- /dev/null +++ b/lib/src/stream/stream_binary_reader.dart @@ -0,0 +1,519 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:typed_data'; + +import '../binary_reader.dart'; + +/// Exception thrown when [StreamBinaryReader] does not have enough data +/// to complete a read operation. +class NotEnoughDataException implements Exception { + /// Creates a new [NotEnoughDataException]. + const NotEnoughDataException(this.required, this.available); + + /// The number of bytes required to complete the operation. + final int required; + + /// The number of bytes currently available in the reader. + final int available; + + @override + String toString() => + 'NotEnoughDataException: required $required bytes, but only ' + '$available available.'; +} + +/// A reader designed for asynchronous streaming data that spans multiple +/// chunks. +/// +/// Unlike [BinaryReader], which requires a single contiguous [Uint8List] +/// buffer, [StreamBinaryReader] manages a queue of chunks. It optimizes reads +/// that fall entirely within a single chunk (zero-copy) and transparently +/// handles reads that cross chunk boundaries. +/// +/// It supports a transactional model ([bookmark], [rollback], [commit]) which +/// is essential for stream parsing when a message might be incomplete. +extension type StreamBinaryReader._(_StreamReaderState _s) { + /// Creates a new [StreamBinaryReader]. + StreamBinaryReader() : this._(_StreamReaderState()); + + /// The total number of unread bytes currently available across all chunks. + int get availableBytes => _s.availableBytes; + + /// Adds a new chunk of data to the reader. + void addChunk(List bytes) { + final chunk = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); + if (chunk.isEmpty) { + return; + } + + _s.chunks.add(chunk); + _s.availableBytes += chunk.length; + + if (_s.currentReader == null) { + final relativeIndex = _s.currentAbsoluteIndex - _s.queueStartIndex; + if (relativeIndex >= 0 && relativeIndex < _s.chunks.length) { + _s.currentReader = BinaryReader(_s.chunks.elementAt(relativeIndex)); + } + } + } + + /// Creates a checkpoint of the current reader state. + /// + /// Use this before attempting to read a message that might be incomplete. + /// If reading fails (e.g., due to [NotEnoughDataException]), you can call + /// [rollback] to restore the state and wait for more data. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void bookmark() { + _s.bookmarks.add(( + absoluteIndex: _s.currentAbsoluteIndex, + readerOffset: _s.currentReader?.offset ?? 0, + availableBytes: _s.availableBytes, + )); + } + + /// Restores the reader to the state of the last [bookmark]. + /// + /// This removes the last bookmark from the stack. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void rollback() { + if (_s.bookmarks.isEmpty) { + throw StateError('No bookmark to rollback to'); + } + + final bm = _s.bookmarks.removeLast(); + _s.currentAbsoluteIndex = bm.absoluteIndex; + _s.availableBytes = bm.availableBytes; + + if (_s.chunks.isNotEmpty) { + final relativeIndex = _s.currentAbsoluteIndex - _s.queueStartIndex; + final targetChunk = _s.chunks.elementAt(relativeIndex); + final cr = _s.currentReader; + if (cr != null) { + cr + ..rebind(targetChunk) + ..seek(bm.readerOffset); + } else { + _s.currentReader = BinaryReader(targetChunk)..seek(bm.readerOffset); + } + } else { + _s.currentReader = null; + } + } + + /// Removes the last [bookmark] without restoring the state. + /// + /// Call this when a message has been successfully and fully parsed. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void commit() { + if (_s.bookmarks.isEmpty) { + throw StateError('No bookmark to commit'); + } + + _s.bookmarks.removeLast(); + _prune(); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _advanceChunk() { + _s.currentAbsoluteIndex++; + if (_s.bookmarks.isEmpty) { + _s.chunks.removeFirst(); + _s.queueStartIndex++; + } + + final relativeIndex = _s.currentAbsoluteIndex - _s.queueStartIndex; + if (relativeIndex < _s.chunks.length) { + _s.currentReader!.rebind(_s.chunks.elementAt(relativeIndex)); + } else { + _s.currentReader = null; + } + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _prune() { + final minNeeded = _s.bookmarks.isEmpty + ? _s.currentAbsoluteIndex + : _s.bookmarks.first.absoluteIndex; + + while (_s.queueStartIndex < minNeeded) { + _s.chunks.removeFirst(); + _s.queueStartIndex++; + } + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _checkAvailable(int length) { + if (_s.availableBytes < length) { + throw NotEnoughDataException(length, _s.availableBytes); + } + } + + /// Reads an 8-bit unsigned integer (0-255). + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readUint8() { + _checkAvailable(1); + final cr = _s.currentReader!; + final val = cr.readUint8(); + _s.availableBytes -= 1; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + + /// Reads an 8-bit signed integer (-128 to 127). + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readInt8() { + _checkAvailable(1); + final cr = _s.currentReader!; + final val = cr.readInt8(); + _s.availableBytes -= 1; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + + /// Reads a boolean value (1 byte). + /// + /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + bool readBool() => readUint8() != 0; + + /// Reads a 16-bit unsigned integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readUint16([Endian endian = Endian.big]) { + _checkAvailable(2); + final cr = _s.currentReader!; + if (cr.availableBytes >= 2) { + final val = cr.readUint16(endian); + _s.availableBytes -= 2; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(2, (data) => data.getUint16(0, endian)); + } + + /// Reads a 16-bit signed integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readInt16([Endian endian = Endian.big]) { + _checkAvailable(2); + final cr = _s.currentReader!; + if (cr.availableBytes >= 2) { + final val = cr.readInt16(endian); + _s.availableBytes -= 2; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(2, (data) => data.getInt16(0, endian)); + } + + /// Reads a 32-bit unsigned integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readUint32([Endian endian = Endian.big]) { + _checkAvailable(4); + final cr = _s.currentReader!; + if (cr.availableBytes >= 4) { + final val = cr.readUint32(endian); + _s.availableBytes -= 4; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(4, (data) => data.getUint32(0, endian)); + } + + /// Reads a 32-bit signed integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readInt32([Endian endian = Endian.big]) { + _checkAvailable(4); + final cr = _s.currentReader!; + if (cr.availableBytes >= 4) { + final val = cr.readInt32(endian); + _s.availableBytes -= 4; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(4, (data) => data.getInt32(0, endian)); + } + + /// Reads a 64-bit unsigned integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readUint64([Endian endian = Endian.big]) { + _checkAvailable(8); + final cr = _s.currentReader!; + if (cr.availableBytes >= 8) { + final val = cr.readUint64(endian); + _s.availableBytes -= 8; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(8, (data) => data.getUint64(0, endian)); + } + + /// Reads a 64-bit signed integer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readInt64([Endian endian = Endian.big]) { + _checkAvailable(8); + final cr = _s.currentReader!; + if (cr.availableBytes >= 8) { + final val = cr.readInt64(endian); + _s.availableBytes -= 8; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(8, (data) => data.getInt64(0, endian)); + } + + /// Reads a 32-bit floating-point number. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double readFloat32([Endian endian = Endian.big]) { + _checkAvailable(4); + final cr = _s.currentReader!; + if (cr.availableBytes >= 4) { + final val = cr.readFloat32(endian); + _s.availableBytes -= 4; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(4, (data) => data.getFloat32(0, endian)); + } + + /// Reads a 64-bit floating-point number. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + double readFloat64([Endian endian = Endian.big]) { + _checkAvailable(8); + final cr = _s.currentReader!; + if (cr.availableBytes >= 8) { + final val = cr.readFloat64(endian); + _s.availableBytes -= 8; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return val; + } + return _readCrossChunk(8, (data) => data.getFloat64(0, endian)); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + T _readCrossChunk(int length, T Function(ByteData) parser) { + final bytes = readBytes(length); + final data = ByteData.sublistView(bytes); + return parser(data); + } + + /// Reads an unsigned variable-length integer (VarInt format). + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readVarUint() { + final cr = _s.currentReader; + if (cr != null && cr.availableBytes >= 10) { + final before = cr.availableBytes; + final value = cr.readVarUint(); + final readLen = before - cr.availableBytes; + _s.availableBytes -= readLen; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return value; + } + + var result = 0; + var shift = 0; + for (var i = 0; i < 10; i++) { + final byte = readUint8(); + result |= (byte & 0x7f) << shift; + if ((byte & 0x80) == 0) { + return result; + } + shift += 7; + } + throw const FormatException('VarInt is too long (more than 10 bytes)'); + } + + /// Reads a signed variable-length integer (ZigZag encoding). + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readVarInt() { + final v = readVarUint(); + return (v >>> 1) ^ -(v & 1); + } + + /// Reads a sequence of bytes. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List readBytes(int length) { + if (length == 0) { + return Uint8List(0); + } + _checkAvailable(length); + + final cr = _s.currentReader!; + if (cr.availableBytes >= length) { + final bytes = cr.readBytes(length); + _s.availableBytes -= length; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return bytes; + } + + final result = Uint8List(length); + var remaining = length; + var resultOffset = 0; + + while (remaining > 0) { + final chunkReader = _s.currentReader!; + final chunkAvailable = chunkReader.availableBytes; + + if (chunkAvailable >= remaining) { + final bytes = chunkReader.readBytes(remaining); + result.setRange(resultOffset, resultOffset + remaining, bytes); + _s.availableBytes -= remaining; + 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; + } + _advanceChunk(); + } + } + return result; + } + + /// Reads all currently available bytes across all chunks. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List readRemainingBytes() => readBytes(_s.availableBytes); + + /// Reads a length-prefixed byte array. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List readVarBytes() { + final length = readVarUint(); + return readBytes(length); + } + + /// Reads a UTF-8 encoded string of the specified byte length. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + String readString(int length, {bool allowMalformed = false}) { + if (length == 0) { + return ''; + } + _checkAvailable(length); + + final cr = _s.currentReader!; + if (cr.availableBytes >= length) { + final value = cr.readString(length, allowMalformed: allowMalformed); + _s.availableBytes -= length; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + return value; + } + + final bytes = readBytes(length); + return utf8.decode(bytes, allowMalformed: allowMalformed); + } + + /// Reads a length-prefixed UTF-8 encoded string. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + String readVarString({bool allowMalformed = false}) { + final length = readVarUint(); + return readString(length, allowMalformed: allowMalformed); + } + + /// Advances the read position by the specified number of bytes. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void skip(int length) { + _checkAvailable(length); + var remaining = length; + + while (remaining > 0) { + final chunkAvailable = _s.currentReader!.availableBytes; + if (chunkAvailable >= remaining) { + _s.currentReader!.skip(remaining); + _s.availableBytes -= remaining; + if (_s.currentReader!.availableBytes == 0) { + _advanceChunk(); + } + break; + } else { + remaining -= chunkAvailable; + _s.availableBytes -= chunkAvailable; + _advanceChunk(); + } + } + } + + /// Checks if there are at least [length] bytes available to read. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + bool hasBytes(int length) { + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } + return _s.availableBytes >= length; + } +} + +typedef _Bookmark = ({int absoluteIndex, int readerOffset, int availableBytes}); + +/// Internal state holder for [StreamBinaryReader]. +final class _StreamReaderState { + _StreamReaderState() + : availableBytes = 0, + currentAbsoluteIndex = 0, + queueStartIndex = 0, + chunks = ListQueue(), + bookmarks = []; + + final ListQueue chunks; + final List<_Bookmark> bookmarks; + + int currentAbsoluteIndex; + int queueStartIndex; + int availableBytes; + BinaryReader? currentReader; +} diff --git a/pubspec.yaml b/pubspec.yaml index 03a3cb2..61be8c9 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: 3.2.0 +version: 3.3.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues diff --git a/test/stream/binary_stream_transformer_test.dart b/test/stream/binary_stream_transformer_test.dart new file mode 100644 index 0000000..78a1e3f --- /dev/null +++ b/test/stream/binary_stream_transformer_test.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +class StringMessage { + StringMessage(this.id, this.text); + final int id; + final String text; +} + +class MyTransformer extends BinaryStreamTransformer { + @override + StringMessage? parse(StreamBinaryReader reader) { + final id = reader.readUint32(); + final text = reader.readVarString(); + return StringMessage(id, text); + } +} + +void main() { + group('BinaryStreamTransformer', () { + test('parses stream of messages across chunks', () async { + final writer = BinaryWriter() + // Message 1 + ..writeUint32(1) + ..writeVarString('Hello') + // Message 2 + ..writeUint32(2) + ..writeVarString('Stream'); + + final allBytes = writer.takeBytes(); + + final controller = StreamController>(); + final stream = controller.stream.transform(MyTransformer()); + + final resultsFuture = stream.toList(); + + for (var i = 0; i < allBytes.length; i++) { + controller.add([allBytes[i]]); + } + + await controller.close(); + final results = await resultsFuture; + + expect(results, hasLength(2)); + expect(results[0].id, equals(1)); + expect(results[0].text, equals('Hello')); + expect(results[1].id, equals(2)); + expect(results[1].text, equals('Stream')); + }); + }); +} diff --git a/test/stream/stream_binary_reader_nested_test.dart b/test/stream/stream_binary_reader_nested_test.dart new file mode 100644 index 0000000..aa90814 --- /dev/null +++ b/test/stream/stream_binary_reader_nested_test.dart @@ -0,0 +1,42 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamBinaryReader - Nested Bookmarks', () { + late StreamBinaryReader reader; + + setUp(() { + reader = StreamBinaryReader(); + }); + + test('nested bookmarks and rollbacks', () { + reader + ..addChunk([1, 2, 3, 4]) + ..bookmark(); // B1 + expect(reader.readUint8(), equals(1)); + + reader.bookmark(); // B2 + expect(reader.readUint8(), equals(2)); + + reader.rollback(); // back to after 1 + expect(reader.readUint8(), equals(2)); + + reader.rollback(); // back to start + expect(reader.readUint8(), equals(1)); + }); + + test('commit and rollback mix', () { + reader + ..addChunk([1, 2, 3]) + ..bookmark() + ..readUint8() // 1 + ..commit() + ..bookmark() + ..readUint8() // 2 + ..rollback(); + + expect(reader.readUint8(), equals(2)); + expect(reader.readUint8(), equals(3)); + }); + }); +} diff --git a/test/stream/stream_binary_reader_test.dart b/test/stream/stream_binary_reader_test.dart new file mode 100644 index 0000000..7adf629 --- /dev/null +++ b/test/stream/stream_binary_reader_test.dart @@ -0,0 +1,79 @@ +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamBinaryReader', () { + late StreamBinaryReader reader; + + setUp(() { + reader = StreamBinaryReader(); + }); + + test('readUint8 across chunks', () { + reader + ..addChunk([1]) + ..addChunk([2]); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + expect(reader.availableBytes, equals(0)); + }); + + test('readUint32 across chunks', () { + reader + ..addChunk([0, 0]) + ..addChunk([0, 42]); + expect(reader.readUint32(), equals(42)); + }); + + test('readVarUint across chunks', () { + // 300 is [0xAC, 0x02] + reader + ..addChunk([0xAC]) + ..addChunk([0x02]); + expect(reader.readVarUint(), equals(300)); + }); + + test('readString across chunks', () { + reader + ..addChunk([72, 101]) // 'He' + ..addChunk([108, 108, 111]); // 'llo' + expect(reader.readString(5), equals('Hello')); + }); + + test('bookmark and rollback', () { + reader + ..addChunk([1, 2, 3]) + ..bookmark(); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + reader.rollback(); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + expect(reader.readUint8(), equals(3)); + }); + + test('NotEnoughDataException', () { + reader.addChunk([1, 2]); + expect(() => reader.readUint32(), throwsA(isA())); + }); + + test('skip across chunks', () { + reader + ..addChunk([1, 2]) + ..addChunk([3, 4]) + ..skip(3); + expect(reader.readUint8(), equals(4)); + }); + + test('readVarString across chunks', () { + final writer = BinaryWriter()..writeVarString('Stream'); + final bytes = writer.takeBytes(); + + reader + ..addChunk(bytes.sublist(0, 3)) + ..addChunk(bytes.sublist(3)); + + expect(reader.readVarString(), equals('Stream')); + }); + }); +} From 77b239745a008e9aef30e2aabe676d3d2a88231e Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 13:10:17 +0300 Subject: [PATCH 06/17] wip: refactoring --- lib/src/stream/stream_binary_reader.dart | 73 +++++++++++++++++------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index 298afbf..6eca73a 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -65,11 +65,15 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void bookmark() { - _s.bookmarks.add(( - absoluteIndex: _s.currentAbsoluteIndex, - readerOffset: _s.currentReader?.offset ?? 0, - availableBytes: _s.availableBytes, - )); + if (_s.bookmarkCount >= _s.bookmarkAbsoluteIndex.length) { + _s._growBookmarks(); + } + + final count = _s.bookmarkCount; + _s.bookmarkAbsoluteIndex[count] = _s.currentAbsoluteIndex; + _s.bookmarkReaderOffset[count] = _s.currentReader?.offset ?? 0; + _s.bookmarkAvailableBytes[count] = _s.availableBytes; + _s.bookmarkCount++; } /// Restores the reader to the state of the last [bookmark]. @@ -78,24 +82,29 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void rollback() { - if (_s.bookmarks.isEmpty) { + if (_s.bookmarkCount == 0) { throw StateError('No bookmark to rollback to'); } - final bm = _s.bookmarks.removeLast(); - _s.currentAbsoluteIndex = bm.absoluteIndex; - _s.availableBytes = bm.availableBytes; + _s.bookmarkCount--; + final count = _s.bookmarkCount; + final absIndex = _s.bookmarkAbsoluteIndex[count]; + final readerOffset = _s.bookmarkReaderOffset[count]; + final availableBytes = _s.bookmarkAvailableBytes[count]; + + _s.currentAbsoluteIndex = absIndex; + _s.availableBytes = availableBytes; if (_s.chunks.isNotEmpty) { - final relativeIndex = _s.currentAbsoluteIndex - _s.queueStartIndex; + final relativeIndex = absIndex - _s.queueStartIndex; final targetChunk = _s.chunks.elementAt(relativeIndex); final cr = _s.currentReader; if (cr != null) { cr ..rebind(targetChunk) - ..seek(bm.readerOffset); + ..seek(readerOffset); } else { - _s.currentReader = BinaryReader(targetChunk)..seek(bm.readerOffset); + _s.currentReader = BinaryReader(targetChunk)..seek(readerOffset); } } else { _s.currentReader = null; @@ -108,11 +117,11 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void commit() { - if (_s.bookmarks.isEmpty) { + if (_s.bookmarkCount == 0) { throw StateError('No bookmark to commit'); } - _s.bookmarks.removeLast(); + _s.bookmarkCount--; _prune(); } @@ -120,7 +129,7 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('dart2js:tryInline') void _advanceChunk() { _s.currentAbsoluteIndex++; - if (_s.bookmarks.isEmpty) { + if (_s.bookmarkCount == 0) { _s.chunks.removeFirst(); _s.queueStartIndex++; } @@ -136,9 +145,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void _prune() { - final minNeeded = _s.bookmarks.isEmpty + final minNeeded = _s.bookmarkCount == 0 ? _s.currentAbsoluteIndex - : _s.bookmarks.first.absoluteIndex; + : _s.bookmarkAbsoluteIndex[0]; while (_s.queueStartIndex < minNeeded) { _s.chunks.removeFirst(); @@ -498,8 +507,6 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } } -typedef _Bookmark = ({int absoluteIndex, int readerOffset, int availableBytes}); - /// Internal state holder for [StreamBinaryReader]. final class _StreamReaderState { _StreamReaderState() @@ -507,13 +514,37 @@ final class _StreamReaderState { currentAbsoluteIndex = 0, queueStartIndex = 0, chunks = ListQueue(), - bookmarks = []; + bookmarkAbsoluteIndex = Int32List(16), + bookmarkReaderOffset = Int32List(16), + bookmarkAvailableBytes = Int32List(16), + bookmarkCount = 0; final ListQueue chunks; - final List<_Bookmark> bookmarks; int currentAbsoluteIndex; int queueStartIndex; int availableBytes; BinaryReader? currentReader; + + // Zero-allocation bookmarks using parallel arrays + Int32List bookmarkAbsoluteIndex; + Int32List bookmarkReaderOffset; + Int32List bookmarkAvailableBytes; + int bookmarkCount; + + @pragma('vm:never-inline') + void _growBookmarks() { + final newCapacity = bookmarkAbsoluteIndex.length * 2; + final newAbsIndex = Int32List(newCapacity); + final newOffset = Int32List(newCapacity); + final newAvail = Int32List(newCapacity); + + newAbsIndex.setRange(0, bookmarkCount, bookmarkAbsoluteIndex); + newOffset.setRange(0, bookmarkCount, bookmarkReaderOffset); + newAvail.setRange(0, bookmarkCount, bookmarkAvailableBytes); + + bookmarkAbsoluteIndex = newAbsIndex; + bookmarkReaderOffset = newOffset; + bookmarkAvailableBytes = newAvail; + } } From b4b9c0ee0ebb7bd0494c57426464cba9abb0ebad Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 13:15:16 +0300 Subject: [PATCH 07/17] wip: errors improvements --- lib/src/stream/stream_binary_reader.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index 6eca73a..1e55282 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -383,6 +383,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readBytes(int length) { + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } if (length == 0) { return Uint8List(0); } @@ -445,6 +448,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') String readString(int length, {bool allowMalformed = false}) { + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } if (length == 0) { return ''; } @@ -476,6 +482,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void skip(int length) { + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } _checkAvailable(length); var remaining = length; From d184620add71d1a1c0dc0f3a8e1ab5c225850171 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 13:30:55 +0300 Subject: [PATCH 08/17] wip: improvements --- lib/src/stream/binary_stream_transformer.dart | 10 +- lib/src/stream/stream_binary_reader.dart | 19 +- .../stream_binary_reader_coverage_test.dart | 218 ++++++++++++++++++ 3 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 test/stream/stream_binary_reader_coverage_test.dart diff --git a/lib/src/stream/binary_stream_transformer.dart b/lib/src/stream/binary_stream_transformer.dart index 9271d8c..31bd88a 100644 --- a/lib/src/stream/binary_stream_transformer.dart +++ b/lib/src/stream/binary_stream_transformer.dart @@ -67,9 +67,11 @@ abstract class BinaryStreamTransformer /// Parses a single message from the [reader]. /// - /// Return the parsed object, or `null` if there is not enough data to - /// complete the message. You can also just read freely, and if a - /// [NotEnoughDataException] is thrown, the transformer will automatically - /// catch it, rollback the reader's state, and wait for more data. + /// Return the parsed object, or `null` if there is not enough data. + /// Alternatively, throw [NotEnoughDataException] explicitly. + /// Both approaches trigger automatic rollback and wait for more data. + /// + /// **Recommendation:** prefer throwing [NotEnoughDataException] for + /// explicit control, or return `null` for simple "not yet ready" cases. T? parse(StreamBinaryReader reader); } diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index 1e55282..e04a29b 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -196,7 +196,17 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - bool readBool() => readUint8() != 0; + bool readBool() { + _checkAvailable(1); + final cr = _s.currentReader!; + final val = cr.readUint8(); + _s.availableBytes -= 1; + if (cr.availableBytes == 0) { + _advanceChunk(); + } + + return val != 0; + } /// Reads a 16-bit unsigned integer. @pragma('vm:prefer-inline') @@ -387,8 +397,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { throw RangeError.value(length, 'length', 'Length must be non-negative'); } if (length == 0) { - return Uint8List(0); + return _emptyBytes; } + _checkAvailable(length); final cr = _s.currentReader!; @@ -398,6 +409,7 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { if (cr.availableBytes == 0) { _advanceChunk(); } + return bytes; } @@ -557,3 +569,6 @@ final class _StreamReaderState { bookmarkAvailableBytes = newAvail; } } + +/// Empty bytes cache +final _emptyBytes = Uint8List(0); diff --git a/test/stream/stream_binary_reader_coverage_test.dart b/test/stream/stream_binary_reader_coverage_test.dart new file mode 100644 index 0000000..89dca8a --- /dev/null +++ b/test/stream/stream_binary_reader_coverage_test.dart @@ -0,0 +1,218 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +void main() { + group('StreamBinaryReader Coverage', () { + late StreamBinaryReader reader; + + setUp(() { + reader = StreamBinaryReader(); + }); + + test('readInt8 across chunks', () { + reader.addChunk([0xFF]); // -1 + expect(reader.readInt8(), equals(-1)); + }); + + test('readBool variations', () { + reader.addChunk([0, 1, 42]); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isTrue); + }); + + test('readInt16 across chunks', () { + reader + ..addChunk([0xFF]) + ..addChunk([0xFF]); + expect(reader.readInt16(), equals(-1)); + }); + + test('readUint16 little-endian across chunks', () { + reader + ..addChunk([0x01]) + ..addChunk([0x00]); + expect(reader.readUint16(Endian.little), equals(1)); + }); + + test('readInt32 across chunks', () { + reader + ..addChunk([0xFF, 0xFF]) + ..addChunk([0xFF, 0xFF]); + expect(reader.readInt32(), equals(-1)); + }); + + test('readUint32 little-endian across chunks', () { + reader + ..addChunk([0x01, 0x00]) + ..addChunk([0x00, 0x00]); + expect(reader.readUint32(Endian.little), equals(1)); + }); + + test('readInt64 across chunks', () { + reader + ..addChunk([0xFF, 0xFF, 0xFF, 0xFF]) + ..addChunk([0xFF, 0xFF, 0xFF, 0xFF]); + expect(reader.readInt64(), equals(-1)); + }); + + test('readUint64 little-endian across chunks', () { + reader + ..addChunk([0x01, 0x00, 0x00, 0x00]) + ..addChunk([0x00, 0x00, 0x00, 0x00]); + expect(reader.readUint64(Endian.little), equals(1)); + }); + + test('readFloat32 across chunks', () { + final writer = BinaryWriter()..writeFloat32(3.14); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 2)) + ..addChunk(bytes.sublist(2)); + expect(reader.readFloat32(), closeTo(3.14, 0.001)); + }); + + test('readFloat64 across chunks', () { + final writer = BinaryWriter()..writeFloat64(3.14159); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 4)) + ..addChunk(bytes.sublist(4)); + expect(reader.readFloat64(), closeTo(3.14159, 0.00001)); + }); + + test('readVarInt across chunks', () { + final writer = BinaryWriter()..writeVarInt(-300); + final bytes = writer.takeBytes(); + for (final b in bytes) { + reader.addChunk([b]); + } + expect(reader.readVarInt(), equals(-300)); + }); + + test('readRemainingBytes across multiple chunks', () { + reader + ..addChunk([1, 2]) + ..addChunk([3, 4]) + ..addChunk([5]); + final bytes = reader.readRemainingBytes(); + expect(bytes, equals([1, 2, 3, 4, 5])); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarBytes across chunks', () { + final writer = BinaryWriter()..writeVarBytes([10, 20, 30]); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 2)) + ..addChunk(bytes.sublist(2)); + expect(reader.readVarBytes(), equals([10, 20, 30])); + }); + + test('readString with allowMalformed across chunks', () { + // Cyrillic 'П' is [0xD0, 0x9F] + reader + ..addChunk([0xD0]) + ..addChunk([0x9F]); + expect(reader.readString(2, allowMalformed: true), equals('П')); + }); + + test('Validation: negative length throws RangeError', () { + expect(() => reader.readBytes(-1), throwsRangeError); + expect(() => reader.readString(-1), throwsRangeError); + expect(() => reader.skip(-1), throwsRangeError); + expect(() => reader.hasBytes(-1), throwsRangeError); + }); + + test('Pruning logic with multiple chunks', () { + reader + ..addChunk([1]) + ..addChunk([2]) + ..addChunk([3]) + ..readUint8() // 1 + ..bookmark() + ..readUint8() // 2 + ..commit(); // Should prune chunk 1 and 2 + + expect(reader.readUint8(), equals(3)); + }); + + test('Bookmark growth logic', () { + // Force bookmark array growth (initial size is 16) + for (var i = 0; i < 20; i++) { + reader.bookmark(); + } + expect(reader.readUint8, throwsA(isA())); + }); + + test('rollback with no currentReader', () { + reader.bookmark(); + expect(() => reader.rollback(), returnsNormally); + }); + + test('readVarUint too long throws FormatException', () { + reader.addChunk(List.filled(11, 0x80)); + expect(() => reader.readVarUint(), throwsFormatException); + }); + }); + + group('BinaryStreamTransformer Coverage', () { + test('catch error in parseLoop', () async { + final controller = StreamController>(); + final transformer = _ErrorTransformer(); + final stream = controller.stream.transform(transformer); + + controller.add([1, 2, 3]); + + expect(stream.first, throwsException); + await controller.close(); + }); + + test('parse returning null waits for more data', () async { + final controller = StreamController>(); + final transformer = _NullTransformer(); + final stream = controller.stream.transform(transformer); + + final results = []; + final sub = stream.listen(results.add); + + controller.add([1]); // Transformer will return null + await Future.delayed(Duration.zero); + expect(results, isEmpty); + + controller.add([2]); // Transformer will return data + await Future.delayed(Duration.zero); + expect(results, equals([42])); + + await controller.close(); + await sub.asFuture(); + await sub.cancel(); + }); + + }); +} + +class _ErrorTransformer extends BinaryStreamTransformer { + @override + int? parse(StreamBinaryReader reader) { + throw Exception('Parse error'); + } +} + +class _NullTransformer extends BinaryStreamTransformer { + var _calls = 0; + @override + int? parse(StreamBinaryReader reader) { + _calls++; + if (_calls == 1) { + return null; + } + reader + ..readUint8() + ..readUint8(); + return 42; + } +} From fa94b71d9e214b53901852ea60c1e571cd2040fe Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 16:14:45 +0300 Subject: [PATCH 09/17] update readme --- README.md | 86 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 828977c..643774e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ * **Zero-Copy Reads**: Operations return `Uint8List` views without allocation. * **One-Pass Strings**: Optimized `writeVarString` with optimistic shift (30% faster). * **Smart Buffering**: Exponential growth (×1.5) and object pooling. -* **Compact Encoding**: VarInt & ZigZag support (Protobuf compatible). +* **Compact Encoding**: VarInt & ZigZag support +* **Stream Parsing**: `StreamBinaryReader` and `BinaryStreamTransformer` for async data. * **Universal**: Supports Native & Web (WASM/JS) with consistent API. * **Modern API**: Leverages Dart Extension Types for zero-overhead abstractions. @@ -19,7 +20,7 @@ ```yaml dependencies: - pro_binary: ^3.2.0 + pro_binary: ^3.3.0 ``` ## Quick Start @@ -46,7 +47,7 @@ final bytesList = [0x01, 0x02, 0x03, 0x04]; final reader2 = BinaryReader.fromList(bytesList); ``` -## Recipes & Patterns +## Recipes & Patterns ### 1. Efficient Object Serialization ```dart @@ -92,47 +93,66 @@ try { } ``` -### 3. Binary Packets (Manual navigation) +### 3. Stream Parsing (Async Binary Messages) +Process binary data arriving in chunks over a stream. + +**Custom Transformer:** +```dart +class MessageParser extends BinaryStreamTransformer { + @override + Message? parse(StreamBinaryReader reader) { + // Return null when not enough data yet + if (!reader.hasBytes(4)) return null; + final id = reader.readUint32(); + final name = reader.readVarString(); + return Message(id, name); + } +} + +// Usage: +stream.transform(MessageParser()).listen((msg) => print(msg)); +``` + +**Manual Chunk Reading:** +```dart +final reader = StreamBinaryReader(); +reader.addChunk(chunk1); +reader.addChunk(chunk2); + +reader.bookmark(); +try { + final id = reader.readUint32(); + final name = reader.readVarString(); + reader.commit(); // Success — consumed +} on NotEnoughDataException { + reader.rollback(); // Wait for more data +} +``` + +### 4. Binary Packets (Manual navigation) ```dart final reader = BinaryReader(bytes); final type = reader[0]; // Absolute peek via operator [] reader.skip(1); if (reader.hasBytes(4)) { - final payload = reader(4); // Concise call syntax for readBytes + final payload = reader(4); // Concise call syntax for readBytes. } ``` -## VarInt Efficiency - -VarInt encoding reduces payload size by up to **75%** for small values: - -| Value | VarInt Size | Fixed Uint32 | Savings | -| :------------------ | :---------- | :----------- | :------ | -| `0..127` | 1 byte | 4 bytes | **75%** | -| `128..16,383` | 2 bytes | 4 bytes | **50%** | -| `16,384..2,097,151` | 3 bytes | 4 bytes | **25%** | - -*Use `writeVarUint` for lengths/counts and `writeVarInt` (ZigZag) for signed deltas.* - -## API Reference Summary - -### **BinaryWriter** -* **Fixed:** `writeUint8`, `writeInt16`, `writeUint32`, `writeInt64`, `writeFloat64`, `writeBool`. -* **Variable:** `writeVarUint` (unsigned), `writeVarInt` (signed). -* **Data:** `writeBytes`, `writeVarBytes`, `writeString`, `writeVarString`. -* **Management:** `takeBytes()` (reset), `toBytes()` (view), `reset()`. +## API Overview -### **BinaryReader** -* **Fixed:** `readUint8`, `readInt16`, `readUint32`, `readInt64`, `readFloat64`, `readBool`. -* **Variable:** `readVarUint`, `readVarInt`. -* **Data:** `readBytes`, `readVarBytes`, `readString`, `readVarString`, `readRemainingBytes`. -* **Navigation:** `skip(n)`, `seek(p)`, `rewind(n)`, `peekBytes(n)`, `peekByte()`, `[index]`. +Full API documentation: https://pub.dev/documentation/pro_binary/latest/pro_binary/ -## Testing & Performance +| Class | Description | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **BinaryWriter** | Encode data: fixed types, VarInt/ZigZag, strings, bytes. Supports `takeBytes()`, `toBytes()`, `reset()`, `seek()`. | +| **BinaryReader** | Decode data: all fixed/variable types, navigation (`skip`, `seek`, `rewind`, `peek`), `rebind()` for reuse. | +| **StreamBinaryReader** | Async streaming: chunk-based reading with `bookmark`/`rollback`/`commit` transactional model. | +| **BinaryStreamTransformer\** | Stream parser: extend and implement `parse()` to process binary streams. | +| **BinaryWriterPool** | Object pool: `acquire()`/`release()` or `withWriter()` for high-frequency writes. | +| **getUtf8Length** | Utility: calculate UTF-8 byte length without encoding. | -We maintain a rigorous test suite: -* **Native (JIT/AOT)**: Optimized for raw performance. -* **Web (WASM/JS)**: Cross-platform consistency. +## Performance Run benchmarks to see it in action: ```bash From 68b5ebe1d3250fe36fc96ae6dcccf72b08eabf7c Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 16:39:01 +0300 Subject: [PATCH 10/17] cleanup --- example/main.dart | 167 ++++++++++++++++++++++++++++++------- lib/src/binary_reader.dart | 41 +++++++-- lib/src/binary_writer.dart | 4 + 3 files changed, 176 insertions(+), 36 deletions(-) diff --git a/example/main.dart b/example/main.dart index 3a55844..6811c77 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,7 +1,117 @@ -// Example demonstrating best practices for using `pro_binary` in a Dart -// ignore_for_file: avoid_print +// Example demonstrating best practices for using `pro_binary` in Dart. +// ignore_for_file: unreachable_from_main +import 'dart:io'; +import 'dart:typed_data'; + import 'package:pro_binary/pro_binary.dart'; +void main() { + // 1. Pool API — recommended for high-frequency writes + log('1. Pool API'); + + final bytes = BinaryWriterPool.withWriter((writer) { + User(id: 101, name: 'Dart 🚀', isActive: true, score: 99.5).encode(writer); + return writer.toBytes(); + }); + + log('Serialized: ${bytes.length} bytes'); + + // 2. Deserialization with navigation + log('\n2. Navigation'); + final reader = BinaryReader(bytes); + + // Peek without consuming + log('First byte: 0x${reader[0].toRadixString(16).padLeft(2, '0')}'); + log('Peek 4 bytes: ${reader.peekBytes(4)}'); + + // Read and navigate + final user = User.decode(reader); + log('Decoded: $user'); + + // Rebind reader to new data (reuse without allocation) + final moreData = Uint8List.fromList([1, 0, 0, 0, 42]); + reader.rebind(moreData); + log('Rebound, read: ${reader.readUint8()}'); + + // 3. Writer reuse with reset + log('\n3. Writer Reuse'); + final writer = BinaryWriter() + ..writeVarUint(1) + ..writeVarString('first'); + final first = writer.takeBytes(); // Resets writer + log('First batch: ${first.length} bytes'); + + writer + ..writeVarUint(2) + ..writeVarString('second'); + final second = writer.takeBytes(); + log('Second batch: ${second.length} bytes'); + + // 4. Signed VarInt (ZigZag) — efficient for deltas + log('\n4. Signed VarInt (ZigZag)'); + final deltaWriter = BinaryWriter(); + for (final delta in [0, -1, 1, -42, 42, -1000, 1000]) { + deltaWriter.writeVarInt(delta); + } + final deltaBytes = deltaWriter.takeBytes(); + log('Encoded 7 deltas in ${deltaBytes.length} bytes'); + + final deltaReader = BinaryReader(deltaBytes); + log('Decoded: ${List.generate(7, (_) => deltaReader.readVarInt())}'); + + // 5. getUtf8Length utility + log('\n5. UTF-8 Length'); + const ascii = 'Hello'; + const unicode = 'Hello 世界 🌍'; + log('"$ascii" -> ${getUtf8Length(ascii)} bytes'); + log('"$unicode" -> ${getUtf8Length(unicode)} bytes'); + + // 6. Pool statistics + log('\n6. Pool Stats'); + final stats = BinaryWriterPool.stats; + log( + 'Pooled: ${stats.pooled}, Hits: ${stats.acquireHit}, ' + 'Misses: ${stats.acquireMiss}', + ); + log('Hit rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%'); + + // 7. Stream parsing (requires actual Stream>) + log('\n7. Stream Parsing'); + // In real usage: + // stream.transform(MessageParser()).listen((msg) => print(msg)); + log('Use: stream.transform(MessageParser()).listen(...)'); + + // 8. Concise callable syntax + log('\n8. Callable Syntax'); + final cWriter = BinaryWriter(); + cWriter([0xAA, 0xBB, 0xCC, 0xDD]); // writeBytes shorthand + final cBytes = cWriter.takeBytes(); + + final cReader = BinaryReader(cBytes); + log('Callable read 2 bytes: ${cReader(2)}'); // readBytes shorthand + + // 9. fromList convenience + log('\n9. List Support'); + final listReader = BinaryReader.fromList([0x01, 0x02, 0x03, 0x04]); + log('From List: ${listReader.readBytes(4)}'); + + // 10. takeBytes vs toBytes + log('\n10. takeBytes vs toBytes'); + final w1 = BinaryWriter()..writeUint32(42); + final view = w1.toBytes(); // View, writer keeps state + w1.writeUint32(100); + log('toBytes() snapshot: ${view.length} bytes'); + log('After more writes: ${w1.takeBytes().length} bytes (writer reset)'); + + final w2 = BinaryWriter()..writeUint32(42); + final owned = w2.takeBytes(); // Resets writer + log('takeBytes() owns buffer: ${owned.length} bytes'); + + log('\nAll examples completed successfully!'); +} + +void log([Object? object = '']) => stdout.writeln(object); + /// A simple domain model to demonstrate serialization best practices. class User { User({ @@ -24,7 +134,7 @@ class User { final bool isActive; final double score; - /// Recommended pattern: Static method for serialization. + /// Recommended pattern: Instance method for serialization. void encode(BinaryWriter w) { w ..writeVarUint(id) @@ -38,34 +148,35 @@ class User { 'User(id: $id, name: "$name", active: $isActive, score: $score)'; } -void main() { - // 1. Using the high-level Pool API (Best for performance) - print('Step 1: Serializing via Pool...'); - final bytes = BinaryWriterPool.withWriter((writer) { - User(id: 101, name: 'Dart 🚀', isActive: true, score: 99.5).encode(writer); - return writer.toBytes(); // View of the pooled buffer - }); - - print('Serialized length: ${bytes.length} bytes\n'); - - // 2. Using the Concise API for reading - print('Step 2: Deserializing...'); - final reader = BinaryReader(bytes); +/// Message for streaming example. +class Message { + Message({required this.type, required this.payload}); - // Concise peek via operator [] - final firstByte = reader[0]; - print('Peek first byte: 0x${firstByte.toRadixString(16).padLeft(2, '0')}'); + factory Message.decode(StreamBinaryReader r) => Message( + type: r.readUint8(), + payload: r.readVarString(), + ); - final decodedUser = User.decode(reader); - print('Decoded user: $decodedUser\n'); + final int type; + final String payload; - // 3. Concise byte operations (Callable syntax) - print('Step 3: Concise data writing...'); - final writer = BinaryWriter(); - writer([0xAA, 0xBB, 0xCC]); // Shorthand for writeBytes + void encode(BinaryWriter w) { + w + ..writeUint8(type) + ..writeVarString(payload); + } - final r2 = BinaryReader(writer.takeBytes()); - print('Read back 2 bytes concisely: ${r2(2)}'); // Shorthand for readBytes(2) + @override + String toString() => 'Message(type: $type, payload: "$payload")'; +} - print('\nAll examples completed successfully!'); +/// Stream parser for [Message]. +class MessageParser extends BinaryStreamTransformer { + @override + Message? parse(StreamBinaryReader reader) { + if (!reader.hasBytes(1)) { + return null; + } + return Message.decode(reader); + } } diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 106f63c..5746708 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -91,8 +91,8 @@ extension type BinaryReader._(_ReaderState _rs) { int readVarUint() { final list = _rs.list; final len = _rs.length; - var offset = _rs.offset; + var offset = _rs.offset; if (offset >= len) { throw RangeError('VarInt out of bounds: offset=$offset length=$len'); } @@ -111,8 +111,10 @@ extension type BinaryReader._(_ReaderState _rs) { if (offset >= len) { throw RangeError('VarInt out of bounds (truncated)'); } + byte = list[offset++]; result |= (byte & 0x7f) << 7; + if ((byte & 0x80) == 0) { _rs.offset = offset; return result; @@ -122,8 +124,10 @@ extension type BinaryReader._(_ReaderState _rs) { if (offset >= len) { throw RangeError('VarInt out of bounds (truncated)'); } + byte = list[offset++]; result |= (byte & 0x7f) << 14; + if ((byte & 0x80) == 0) { _rs.offset = offset; return result; @@ -135,6 +139,7 @@ extension type BinaryReader._(_ReaderState _rs) { if (offset >= len) { throw RangeError('VarInt out of bounds (truncated)'); } + byte = list[offset++]; result |= (byte & 0x7f) << shift; @@ -142,6 +147,7 @@ extension type BinaryReader._(_ReaderState _rs) { _rs.offset = offset; return result; } + shift += 7; } @@ -264,6 +270,7 @@ extension type BinaryReader._(_ReaderState _rs) { final value = _rs.data.getUint32(_rs.offset, endian); _rs.offset += 4; + return value; } @@ -281,8 +288,10 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); + final value = _rs.data.getInt32(_rs.offset, endian); _rs.offset += 4; + return value; } @@ -305,8 +314,10 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); + final value = _rs.data.getUint64(_rs.offset, endian); _rs.offset += 8; + return value; } @@ -326,8 +337,10 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); + final value = _rs.data.getInt64(_rs.offset, endian); _rs.offset += 8; + return value; } @@ -369,6 +382,7 @@ extension type BinaryReader._(_ReaderState _rs) { final value = _rs.data.getFloat64(_rs.offset, endian); _rs.offset += 8; + return value; } @@ -394,6 +408,7 @@ extension type BinaryReader._(_ReaderState _rs) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); } + _checkBounds(length, 'Bytes'); // Create a view of the underlying buffer without copying @@ -446,6 +461,7 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') Uint8List readVarBytes() { final length = readVarUint(); + return readBytes(length); } @@ -508,6 +524,7 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') String readVarString({bool allowMalformed = false}) { final length = readVarUint(); + return readString(length, allowMalformed: allowMalformed); } @@ -524,6 +541,7 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') bool readBool() { final value = readUint8(); + return value != 0; } @@ -547,6 +565,7 @@ extension type BinaryReader._(_ReaderState _rs) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); } + return (_rs.offset + length) <= _rs.length; } @@ -585,8 +604,9 @@ extension type BinaryReader._(_ReaderState _rs) { _checkBounds(length, 'Peek Bytes', peekOffset); final bOffset = _rs.baseOffset; + final bytes = _rs.data.buffer.asUint8List(bOffset + peekOffset, length); - return _rs.data.buffer.asUint8List(bOffset + peekOffset, length); + return bytes; } /// Returns the byte at the current read position without advancing the @@ -606,6 +626,7 @@ extension type BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int peekByte() { _checkBounds(1, 'Peek Byte'); + return _rs.list[_rs.offset]; } @@ -630,6 +651,7 @@ extension type BinaryReader._(_ReaderState _rs) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); } + _checkBounds(length, 'Skip'); _rs.offset += length; @@ -652,6 +674,7 @@ extension type BinaryReader._(_ReaderState _rs) { if (position < 0 || position > _rs.length) { throw RangeError.range(position, 0, _rs.length, 'position'); } + _rs.offset = position; } @@ -672,11 +695,13 @@ extension type BinaryReader._(_ReaderState _rs) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); } + if (_rs.offset - length < 0) { throw RangeError( 'Cannot rewind $length bytes from offset ${_rs.offset}', ); } + _rs.offset -= length; } @@ -778,12 +803,12 @@ final class _ReaderState { /// The old buffer is discarded and becomes eligible for GC. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void rebind(Uint8List buffer) { - list = buffer; - data = ByteData.sublistView(buffer).asUnmodifiableView(); - this.buffer = buffer.buffer; - length = buffer.length; - baseOffset = buffer.offsetInBytes; + void rebind(Uint8List newBuffer) { + list = newBuffer; + data = ByteData.sublistView(newBuffer).asUnmodifiableView(); + buffer = newBuffer.buffer; + length = newBuffer.length; + baseOffset = newBuffer.offsetInBytes; offset = 0; } } diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 2311a52..303071d 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -337,6 +337,7 @@ extension type BinaryWriter._(_WriterState _ws) { @pragma('dart2js:tryInline') void writeFloat32(double value, [Endian endian = .big]) { _ws.ensureFourBytes(); + _ws.data.setFloat32(_ws.offset, value, endian); _ws.offset += 4; } @@ -353,6 +354,7 @@ extension type BinaryWriter._(_WriterState _ws) { @pragma('dart2js:tryInline') void writeFloat64(double value, [Endian endian = .big]) { _ws.ensureEightBytes(); + _ws.data.setFloat64(_ws.offset, value, endian); _ws.offset += 8; } @@ -375,6 +377,7 @@ extension type BinaryWriter._(_WriterState _ws) { if (offset < 0) { throw RangeError.value(offset, 'offset', 'Offset must be non-negative'); } + if (offset > bytes.length) { throw RangeError.range(offset, 0, bytes.length, 'offset'); } @@ -588,6 +591,7 @@ extension type BinaryWriter._(_WriterState _ws) { final len = value.length; if (len == 0) { writeVarUint(0); + return; } From 4b0381b3f3b668918d8375caf9a6db5895e0e968 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 17:14:06 +0300 Subject: [PATCH 11/17] wip --- CHANGELOG.md | 12 +++++------- lib/src/stream/binary_stream_transformer.dart | 6 ++++++ lib/src/stream/stream_binary_reader.dart | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be425a..d6355d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 3.3.0 +## 4.0.0 **BREAKING CHANGES:** @@ -10,17 +10,15 @@ - **BinaryWriter**: added `seek(int position)` — sets the write position to the specified byte offset (useful for backtracking and overwriting data mid-stream) - **BinaryWriter**: added `writeUint8At(int position, int value)` — writes a byte at the specified position without changing the current write position - **BinaryWriter**: `writeVarString` now uses `seek` internally for VarInt length rewriting +- **Stream API**: added `StreamBinaryReader` — chunk-based reader for asynchronous streaming data with bookmark/rollback/commit transactional model +- **Stream API**: added `BinaryStreamTransformer` — abstract `StreamTransformer` for parsing binary messages from streams with automatic `NotEnoughDataException` handling +- **Stream API**: added `NotEnoughDataException` — carries `required`/`available` byte counts for debugging incomplete data scenarios **Fixes:** - **BinaryReader**: added bounds check to `peekByte()` — now throws `RangeError` consistently like other read methods -**Tests:** - -- Added tests for `BinaryReader.rebind()` — normal rebind, partial reads, zero-length buffer, identity preservation, multiple rebinds, non-zero buffer offset -- Added tests for `BinaryWriter.seek()` — seek to position 0, middle, end, negative position, beyond bytesWritten, overwrite, preserve bytesWritten -- Added tests for `BinaryWriter.writeUint8At()` — overwrite at position 0/middle/end, no change to bytesWritten/write position, negative position, beyond bytesWritten, value exceeding 255, negative value, empty writer -- Added integration tests for `writeVarString` with `seek` — ASCII, non-ASCII, emoji +**Tests:** Improved all tests ## 3.2.0 diff --git a/lib/src/stream/binary_stream_transformer.dart b/lib/src/stream/binary_stream_transformer.dart index 31bd88a..c5d8132 100644 --- a/lib/src/stream/binary_stream_transformer.dart +++ b/lib/src/stream/binary_stream_transformer.dart @@ -46,6 +46,7 @@ abstract class BinaryStreamTransformer Stream _parseLoop(StreamBinaryReader reader) async* { while (reader.availableBytes > 0) { reader.bookmark(); + final bytesBefore = reader.availableBytes; try { final result = parse(reader); if (result == null) { @@ -53,6 +54,11 @@ abstract class BinaryStreamTransformer break; // Wait for more data } else { reader.commit(); + if (reader.availableBytes == bytesBefore) { + // parse() returned a result without consuming any data — + // break to avoid an infinite loop + break; + } yield result; } } on NotEnoughDataException { diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index e04a29b..95cba0b 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -168,6 +168,7 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { @pragma('dart2js:tryInline') int readUint8() { _checkAvailable(1); + // Invariant: availableBytes > 0 implies currentReader != null final cr = _s.currentReader!; final val = cr.readUint8(); _s.availableBytes -= 1; From e2422385d71af13ed5c8af30cb06ca55a5d4151a Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 17:54:46 +0300 Subject: [PATCH 12/17] rename some tests --- lib/src/binary_writer.dart | 2 +- lib/src/stream/binary_stream_transformer.dart | 2 +- .../advanced_features_integration_test.dart | 8 +- test/integration/basic_integration_test.dart | 12 +- .../complex_structures_integration_test.dart | 6 +- .../robustness_integration_test.dart | 8 +- test/integration/types_integration_test.dart | 14 +- .../binary_stream_transformer_test.dart | 4 +- .../stream_binary_reader_coverage_test.dart | 276 ++++++++++++++++-- .../stream_binary_reader_nested_test.dart | 6 +- test/stream/stream_binary_reader_test.dart | 18 +- test/unit/binary_reader_basic_test.dart | 14 +- test/unit/binary_reader_edge_cases_test.dart | 8 +- test/unit/binary_reader_navigation_test.dart | 8 +- test/unit/binary_reader_string_test.dart | 2 +- test/unit/binary_writer_basic_test.dart | 10 +- test/unit/binary_writer_edge_cases_test.dart | 6 +- test/unit/binary_writer_string_test.dart | 8 +- 18 files changed, 321 insertions(+), 91 deletions(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 303071d..862677b 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -377,7 +377,7 @@ extension type BinaryWriter._(_WriterState _ws) { if (offset < 0) { throw RangeError.value(offset, 'offset', 'Offset must be non-negative'); } - + if (offset > bytes.length) { throw RangeError.range(offset, 0, bytes.length, 'offset'); } diff --git a/lib/src/stream/binary_stream_transformer.dart b/lib/src/stream/binary_stream_transformer.dart index c5d8132..365046c 100644 --- a/lib/src/stream/binary_stream_transformer.dart +++ b/lib/src/stream/binary_stream_transformer.dart @@ -54,12 +54,12 @@ abstract class BinaryStreamTransformer break; // Wait for more data } else { reader.commit(); + yield result; if (reader.availableBytes == bytesBefore) { // parse() returned a result without consuming any data — // break to avoid an infinite loop break; } - yield result; } } on NotEnoughDataException { reader.rollback(); diff --git a/test/integration/advanced_features_integration_test.dart b/test/integration/advanced_features_integration_test.dart index bc4146d..2f59621 100644 --- a/test/integration/advanced_features_integration_test.dart +++ b/test/integration/advanced_features_integration_test.dart @@ -2,8 +2,8 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('Advanced Features Integration Tests', () { - test('reader navigation after complex write', () { + group('Integration Advanced Features', () { + test('handles reader navigation after complex write', () { final writer = BinaryWriter() ..writeUint32(1) ..writeUint32(2) @@ -15,7 +15,7 @@ void main() { expect(reader.readUint32(), equals(2)); }); - test('multiple readers on same data', () { + test('supports multiple readers on same data', () { final writer = BinaryWriter() ..writeUint32(100) ..writeUint32(200); @@ -30,7 +30,7 @@ void main() { expect(reader2.readUint32(), equals(200)); }); - test('writer buffer management - toBytes preserves state', () { + test('toBytes preserves writer state', () { final writer = BinaryWriter()..writeUint32(100); final bytes1 = writer.toBytes(); expect(bytes1, equals([0, 0, 0, 100])); diff --git a/test/integration/basic_integration_test.dart b/test/integration/basic_integration_test.dart index 559b9c6..5cabcce 100644 --- a/test/integration/basic_integration_test.dart +++ b/test/integration/basic_integration_test.dart @@ -2,32 +2,32 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('Basic Integration Tests', () { - test('write and read single Uint8', () { + group('Integration Basic Types', () { + test('writes and reads single Uint8', () { final writer = BinaryWriter()..writeUint8(42); final reader = BinaryReader(writer.takeBytes()); expect(reader.readUint8(), equals(42)); }); - test('write and read single Int8', () { + test('writes and reads single Int8', () { final writer = BinaryWriter()..writeInt8(-42); final reader = BinaryReader(writer.takeBytes()); expect(reader.readInt8(), equals(-42)); }); - test('write and read Uint16 with big-endian', () { + test('writes and reads Uint16 with big-endian', () { final writer = BinaryWriter()..writeUint16(65535); final reader = BinaryReader(writer.takeBytes()); expect(reader.readUint16(), equals(65535)); }); - test('write and read Float32', () { + test('writes and reads Float32', () { final writer = BinaryWriter()..writeFloat32(3.14159); final reader = BinaryReader(writer.takeBytes()); expect(reader.readFloat32(), closeTo(3.14159, 0.00001)); }); - test('write and read basic cycles for all fixed types', () { + test('round-trip writes and reads all fixed types', () { final writer = BinaryWriter() ..writeUint8(1) ..writeInt8(-1) diff --git a/test/integration/complex_structures_integration_test.dart b/test/integration/complex_structures_integration_test.dart index f36579a..fd80297 100644 --- a/test/integration/complex_structures_integration_test.dart +++ b/test/integration/complex_structures_integration_test.dart @@ -2,8 +2,8 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('Complex Structures Integration Tests', () { - test('write and read sequence of different types', () { + group('Integration Complex Structures', () { + test('round-trip writes and reads sequence of different types', () { final writer = BinaryWriter() ..writeUint8(255) ..writeInt8(-128) @@ -30,7 +30,7 @@ void main() { }); group('Real-world message format simulation', () { - test('protocol with header and payload', () { + test('parses protocol with header and payload', () { final writer = BinaryWriter() // Header ..writeUint8(1) // version diff --git a/test/integration/robustness_integration_test.dart b/test/integration/robustness_integration_test.dart index d7f6f80..757cef0 100644 --- a/test/integration/robustness_integration_test.dart +++ b/test/integration/robustness_integration_test.dart @@ -2,8 +2,8 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('Robustness Integration Tests', () { - test('Round-trip validation for all types', () { + group('Integration Robustness', () { + test('validates round-trip for all types', () { final writer = BinaryWriter() ..writeUint8(255) ..writeInt16(-32768) @@ -21,7 +21,7 @@ void main() { expect(reader.readBool(), isTrue); }); - test('Stress test - many small operations', () { + test('handles stress test with many small operations', () { final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { writer.writeUint8(i % 256); @@ -33,7 +33,7 @@ void main() { } }); - test('Boundary conditions - writing exactly to buffer boundary', () { + test('handles boundary conditions writing exactly to buffer boundary', () { final writer = BinaryWriter(initialBufferSize: 8) // // ignore: avoid_js_rounded_ints diff --git a/test/integration/types_integration_test.dart b/test/integration/types_integration_test.dart index 2705c71..09dee51 100644 --- a/test/integration/types_integration_test.dart +++ b/test/integration/types_integration_test.dart @@ -4,9 +4,9 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('Integrated Types Tests', () { + group('Integration Types', () { group('String handling', () { - test('write and read mixed Unicode string', () { + test('round-trip writes and reads mixed Unicode string', () { final writer = BinaryWriter(); const str = 'ASCII_Юникод_中文_🌍'; writer.writeString(str); @@ -16,7 +16,7 @@ void main() { }); group('Float special values', () { - test('write and read special floats', () { + test('round-trip writes and reads special floats', () { final writer = BinaryWriter() ..writeFloat32(double.nan) ..writeFloat64(double.infinity); @@ -27,7 +27,7 @@ void main() { }); group('Variable-length types', () { - test('write and read VarUint and VarInt', () { + test('round-trip writes and reads VarUint and VarInt', () { final writer = BinaryWriter() ..writeVarUint(300) ..writeVarInt(-100); @@ -36,7 +36,7 @@ void main() { expect(reader.readVarInt(), equals(-100)); }); - test('write and read VarBytes and VarString', () { + test('round-trip writes and reads VarBytes and VarString', () { final writer = BinaryWriter() ..writeVarBytes([1, 2, 3]) ..writeVarString('Hello'); @@ -47,7 +47,7 @@ void main() { }); group('Boolean operations', () { - test('write and read multiple booleans', () { + test('round-trip writes and reads multiple booleans', () { final writer = BinaryWriter() ..writeBool(true) ..writeBool(false) @@ -60,7 +60,7 @@ void main() { }); group('Large data cycles', () { - test('write and read large data set', () { + test('round-trip writes and reads large data set', () { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(1000, (i) => i % 256)); writer.writeBytes(data); diff --git a/test/stream/binary_stream_transformer_test.dart b/test/stream/binary_stream_transformer_test.dart index 78a1e3f..625ea70 100644 --- a/test/stream/binary_stream_transformer_test.dart +++ b/test/stream/binary_stream_transformer_test.dart @@ -19,8 +19,8 @@ class MyTransformer extends BinaryStreamTransformer { } void main() { - group('BinaryStreamTransformer', () { - test('parses stream of messages across chunks', () async { + group('BinaryStreamTransformer Message Parsing', () { + test('parses messages from stream across chunks', () async { final writer = BinaryWriter() // Message 1 ..writeUint32(1) diff --git a/test/stream/stream_binary_reader_coverage_test.dart b/test/stream/stream_binary_reader_coverage_test.dart index 89dca8a..f76581e 100644 --- a/test/stream/stream_binary_reader_coverage_test.dart +++ b/test/stream/stream_binary_reader_coverage_test.dart @@ -5,68 +5,68 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('StreamBinaryReader Coverage', () { + group('StreamBinaryReader Chunk Operations', () { late StreamBinaryReader reader; setUp(() { reader = StreamBinaryReader(); }); - test('readInt8 across chunks', () { + test('readInt8 handles chunk boundary', () { reader.addChunk([0xFF]); // -1 expect(reader.readInt8(), equals(-1)); }); - test('readBool variations', () { + test('readBool reads boolean values', () { reader.addChunk([0, 1, 42]); expect(reader.readBool(), isFalse); expect(reader.readBool(), isTrue); expect(reader.readBool(), isTrue); }); - test('readInt16 across chunks', () { + test('readInt16 handles chunk boundary', () { reader ..addChunk([0xFF]) ..addChunk([0xFF]); expect(reader.readInt16(), equals(-1)); }); - test('readUint16 little-endian across chunks', () { + test('readUint16 supports little-endian', () { reader ..addChunk([0x01]) ..addChunk([0x00]); expect(reader.readUint16(Endian.little), equals(1)); }); - test('readInt32 across chunks', () { + test('readInt32 handles chunk boundary', () { reader ..addChunk([0xFF, 0xFF]) ..addChunk([0xFF, 0xFF]); expect(reader.readInt32(), equals(-1)); }); - test('readUint32 little-endian across chunks', () { + test('readUint32 supports little-endian', () { reader ..addChunk([0x01, 0x00]) ..addChunk([0x00, 0x00]); expect(reader.readUint32(Endian.little), equals(1)); }); - test('readInt64 across chunks', () { + test('readInt64 handles chunk boundary', () { reader ..addChunk([0xFF, 0xFF, 0xFF, 0xFF]) ..addChunk([0xFF, 0xFF, 0xFF, 0xFF]); expect(reader.readInt64(), equals(-1)); }); - test('readUint64 little-endian across chunks', () { + test('readUint64 supports little-endian', () { reader ..addChunk([0x01, 0x00, 0x00, 0x00]) ..addChunk([0x00, 0x00, 0x00, 0x00]); expect(reader.readUint64(Endian.little), equals(1)); }); - test('readFloat32 across chunks', () { + test('readFloat32 handles chunk boundary', () { final writer = BinaryWriter()..writeFloat32(3.14); final bytes = writer.takeBytes(); reader @@ -75,7 +75,7 @@ void main() { expect(reader.readFloat32(), closeTo(3.14, 0.001)); }); - test('readFloat64 across chunks', () { + test('readFloat64 handles chunk boundary', () { final writer = BinaryWriter()..writeFloat64(3.14159); final bytes = writer.takeBytes(); reader @@ -84,7 +84,7 @@ void main() { expect(reader.readFloat64(), closeTo(3.14159, 0.00001)); }); - test('readVarInt across chunks', () { + test('readVarInt handles chunk boundary', () { final writer = BinaryWriter()..writeVarInt(-300); final bytes = writer.takeBytes(); for (final b in bytes) { @@ -93,7 +93,7 @@ void main() { expect(reader.readVarInt(), equals(-300)); }); - test('readRemainingBytes across multiple chunks', () { + test('readRemainingBytes reads from multiple chunks', () { reader ..addChunk([1, 2]) ..addChunk([3, 4]) @@ -103,7 +103,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readVarBytes across chunks', () { + test('readVarBytes handles chunk boundary', () { final writer = BinaryWriter()..writeVarBytes([10, 20, 30]); final bytes = writer.takeBytes(); reader @@ -112,7 +112,7 @@ void main() { expect(reader.readVarBytes(), equals([10, 20, 30])); }); - test('readString with allowMalformed across chunks', () { + test('readString supports allowMalformed across chunk boundary', () { // Cyrillic 'П' is [0xD0, 0x9F] reader ..addChunk([0xD0]) @@ -120,14 +120,14 @@ void main() { expect(reader.readString(2, allowMalformed: true), equals('П')); }); - test('Validation: negative length throws RangeError', () { + test('readBytes throws RangeError for negative length', () { expect(() => reader.readBytes(-1), throwsRangeError); expect(() => reader.readString(-1), throwsRangeError); expect(() => reader.skip(-1), throwsRangeError); expect(() => reader.hasBytes(-1), throwsRangeError); }); - test('Pruning logic with multiple chunks', () { + test('commit prunes consumed chunks', () { reader ..addChunk([1]) ..addChunk([2]) @@ -140,7 +140,7 @@ void main() { expect(reader.readUint8(), equals(3)); }); - test('Bookmark growth logic', () { + test('bookmark handles growth beyond initial capacity', () { // Force bookmark array growth (initial size is 16) for (var i = 0; i < 20; i++) { reader.bookmark(); @@ -148,19 +148,171 @@ void main() { expect(reader.readUint8, throwsA(isA())); }); - test('rollback with no currentReader', () { + test('rollback handles no current reader', () { reader.bookmark(); expect(() => reader.rollback(), returnsNormally); }); - test('readVarUint too long throws FormatException', () { + test('readVarUint throws FormatException for oversized varint', () { reader.addChunk(List.filled(11, 0x80)); expect(() => reader.readVarUint(), throwsFormatException); }); + + test('addChunk ignores empty list', () { + reader.addChunk([]); + expect(reader.availableBytes, equals(0)); + }); + + test('addChunk accepts List', () { + final list = [1, 2, 3]; + reader.addChunk(list); + expect(reader.availableBytes, equals(3)); + expect(reader.readUint8(), equals(1)); + }); + + test('readBytes handles zero length', () { + reader.addChunk([1, 2, 3]); + final bytes = reader.readBytes(0); + expect(bytes, isEmpty); + expect(reader.availableBytes, equals(3)); + }); + + test('readRemainingBytes returns empty when no data', () { + final bytes = reader.readRemainingBytes(); + expect(bytes, isEmpty); + }); + + test('skip handles zero offset', () { + reader + ..addChunk([1, 2, 3]) + ..skip(0); + expect(reader.readUint8(), equals(1)); + }); + + test('hasBytes returns true for zero length', () { + expect(reader.hasBytes(0), isTrue); + reader.addChunk([1, 2, 3]); + expect(reader.hasBytes(0), isTrue); + }); + + test('readString handles zero length', () { + reader.addChunk([1, 2, 3]); + expect(reader.readString(0), equals('')); + expect(reader.availableBytes, equals(3)); + }); + + test('readVarString handles empty string', () { + reader.addChunk([0]); + expect(reader.readVarString(), equals('')); + }); + + test('readUint16 supports little-endian', () { + reader + ..addChunk([0x34]) + ..addChunk([0x12]); + expect(reader.readUint16(Endian.little), equals(0x1234)); + }); + + test('readInt16 supports little-endian', () { + final writer = BinaryWriter()..writeInt16(-852, Endian.little); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 1)) + ..addChunk(bytes.sublist(1)); + expect(reader.readInt16(Endian.little), equals(-852)); + }); + + test('readUint32 supports little-endian', () { + final writer = BinaryWriter()..writeUint32(0x44332211, Endian.little); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 2)) + ..addChunk(bytes.sublist(2)); + expect(reader.readUint32(Endian.little), equals(0x44332211)); + }); + + test('readInt32 supports little-endian', () { + final writer = BinaryWriter()..writeInt32(-266, Endian.little); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 2)) + ..addChunk(bytes.sublist(2)); + expect(reader.readInt32(Endian.little), equals(-266)); + }); + + test('readUint64 supports little-endian', () { + reader + ..addChunk([0x01]) + ..addChunk([0x00]) + ..addChunk([0x00]) + ..addChunk([0x00]) + ..addChunk([0x00]) + ..addChunk([0x00]) + ..addChunk([0x00]) + ..addChunk([0x00]); + expect(reader.readUint64(Endian.little), equals(1)); + }); + + test('readInt64 supports little-endian', () { + reader + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]) + ..addChunk([0xFF]); + expect(reader.readInt64(Endian.little), equals(-1)); + }); + + test('readFloat32 supports little-endian', () { + final writer = BinaryWriter()..writeFloat32(2.5, Endian.little); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 2)) + ..addChunk(bytes.sublist(2)); + expect(reader.readFloat32(Endian.little), closeTo(2.5, 0.001)); + }); + + test('readFloat64 supports little-endian', () { + final writer = BinaryWriter()..writeFloat64(3.14159, Endian.little); + final bytes = writer.takeBytes(); + reader + ..addChunk(bytes.sublist(0, 4)) + ..addChunk(bytes.sublist(4)); + expect(reader.readFloat64(Endian.little), closeTo(3.14159, 0.00001)); + }); + + test('readBytes handles more than two chunks', () { + reader + ..addChunk([1, 2]) + ..addChunk([3, 4]) + ..addChunk([5, 6]); + final bytes = reader.readBytes(5); + expect(bytes, equals([1, 2, 3, 4, 5])); + }); + + test('NotEnoughDataException toString includes required and available', () { + reader.addChunk([1, 2]); + expect( + () => reader.readUint32(), + throwsA( + isA().having( + (e) => e.toString(), + 'toString', + allOf( + contains('required 4 bytes'), + contains('2 available'), + ), + ), + ), + ); + }); }); - group('BinaryStreamTransformer Coverage', () { - test('catch error in parseLoop', () async { + group('BinaryStreamTransformer Stream Behavior', () { + test('rethrows non-NotEnoughDataException errors', () async { final controller = StreamController>(); final transformer = _ErrorTransformer(); final stream = controller.stream.transform(transformer); @@ -171,7 +323,7 @@ void main() { await controller.close(); }); - test('parse returning null waits for more data', () async { + test('waits for more data when parse returns null', () async { final controller = StreamController>(); final transformer = _NullTransformer(); final stream = controller.stream.transform(transformer); @@ -192,6 +344,66 @@ void main() { await sub.cancel(); }); + test( + 'breaks loop when parse returns without consuming data', + () async { + final controller = StreamController>(); + final transformer = _ZeroByteTransformer(); + final stream = controller.stream.transform(transformer); + + final results = []; + final sub = stream.listen(results.add); + + controller.add([1, 2, 3]); + await Future.delayed(Duration.zero); + expect(results, equals([42])); + + await controller.close(); + await sub.asFuture(); + await sub.cancel(); + }, + ); + + test('ignores empty chunk', () async { + final controller = StreamController>(); + final transformer = _TwoByteTransformer(); + final stream = controller.stream.transform(transformer); + + final results = []; + final sub = stream.listen(results.add); + + controller.add([]); + await Future.delayed(Duration.zero); + + controller.add([1]); + await Future.delayed(Duration.zero); + + controller.add([2]); + await Future.delayed(Duration.zero); + expect(results, equals([42])); + + await controller.close(); + await sub.asFuture(); + await sub.cancel(); + }); + + test('accepts List chunk', () async { + final controller = StreamController>(); + final transformer = _TwoByteTransformer(); + final stream = controller.stream.transform(transformer); + + final data = [1, 2]; + final results = []; + final sub = stream.listen(results.add); + + controller.add(data); + await Future.delayed(Duration.zero); + expect(results, equals([42])); + + await controller.close(); + await sub.asFuture(); + await sub.cancel(); + }); }); } @@ -216,3 +428,21 @@ class _NullTransformer extends BinaryStreamTransformer { return 42; } } + +class _ZeroByteTransformer extends BinaryStreamTransformer { + @override + int? parse(StreamBinaryReader reader) => 42; +} + +class _TwoByteTransformer extends BinaryStreamTransformer { + @override + int? parse(StreamBinaryReader reader) { + if (reader.availableBytes < 2) { + return null; + } + reader + ..readUint8() + ..readUint8(); + return 42; + } +} diff --git a/test/stream/stream_binary_reader_nested_test.dart b/test/stream/stream_binary_reader_nested_test.dart index aa90814..de7885e 100644 --- a/test/stream/stream_binary_reader_nested_test.dart +++ b/test/stream/stream_binary_reader_nested_test.dart @@ -2,14 +2,14 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('StreamBinaryReader - Nested Bookmarks', () { + group('StreamBinaryReader Bookmark Operations', () { late StreamBinaryReader reader; setUp(() { reader = StreamBinaryReader(); }); - test('nested bookmarks and rollbacks', () { + test('supports nested bookmarks with rollback', () { reader ..addChunk([1, 2, 3, 4]) ..bookmark(); // B1 @@ -25,7 +25,7 @@ void main() { expect(reader.readUint8(), equals(1)); }); - test('commit and rollback mix', () { + test('handles commit and rollback mix', () { reader ..addChunk([1, 2, 3]) ..bookmark() diff --git a/test/stream/stream_binary_reader_test.dart b/test/stream/stream_binary_reader_test.dart index 7adf629..37f5375 100644 --- a/test/stream/stream_binary_reader_test.dart +++ b/test/stream/stream_binary_reader_test.dart @@ -2,14 +2,14 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('StreamBinaryReader', () { + group('StreamBinaryReader Chunk Operations', () { late StreamBinaryReader reader; setUp(() { reader = StreamBinaryReader(); }); - test('readUint8 across chunks', () { + test('readUint8 handles chunk boundary', () { reader ..addChunk([1]) ..addChunk([2]); @@ -18,14 +18,14 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint32 across chunks', () { + test('readUint32 handles chunk boundary', () { reader ..addChunk([0, 0]) ..addChunk([0, 42]); expect(reader.readUint32(), equals(42)); }); - test('readVarUint across chunks', () { + test('readVarUint handles chunk boundary', () { // 300 is [0xAC, 0x02] reader ..addChunk([0xAC]) @@ -33,14 +33,14 @@ void main() { expect(reader.readVarUint(), equals(300)); }); - test('readString across chunks', () { + test('readString handles chunk boundary', () { reader ..addChunk([72, 101]) // 'He' ..addChunk([108, 108, 111]); // 'llo' expect(reader.readString(5), equals('Hello')); }); - test('bookmark and rollback', () { + test('bookmark and rollback preserves state', () { reader ..addChunk([1, 2, 3]) ..bookmark(); @@ -52,12 +52,12 @@ void main() { expect(reader.readUint8(), equals(3)); }); - test('NotEnoughDataException', () { + test('NotEnoughDataException thrown when insufficient data', () { reader.addChunk([1, 2]); expect(() => reader.readUint32(), throwsA(isA())); }); - test('skip across chunks', () { + test('skip handles chunk boundary', () { reader ..addChunk([1, 2]) ..addChunk([3, 4]) @@ -65,7 +65,7 @@ void main() { expect(reader.readUint8(), equals(4)); }); - test('readVarString across chunks', () { + test('readVarString handles chunk boundary', () { final writer = BinaryWriter()..writeVarString('Stream'); final bytes = writer.takeBytes(); diff --git a/test/unit/binary_reader_basic_test.dart b/test/unit/binary_reader_basic_test.dart index 8bfb88b..44ef1f3 100644 --- a/test/unit/binary_reader_basic_test.dart +++ b/test/unit/binary_reader_basic_test.dart @@ -5,14 +5,14 @@ import 'package:test/test.dart'; void main() { group('BinaryReader Basic Operations', () { - test('reads Uint8 correctly', () { + test('reads Uint8 value from single byte', () { final buffer = Uint8List.fromList([0x01]); final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(1)); expect(reader.availableBytes, equals(0)); }); - test('reads Int8 correctly', () { + test('reads Int8 value from single byte', () { final buffer = Uint8List.fromList([0xFF]); final reader = BinaryReader(buffer); expect(reader.readInt8(), equals(-1)); @@ -31,25 +31,25 @@ void main() { expect(reader.readUint16(.little), equals(256)); }); - test('reads Uint32 correctly', () { + test('reads Uint32 value from four bytes', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); final reader = BinaryReader(buffer); expect(reader.readUint32(), equals(65536)); }); - test('reads Uint64 correctly', () { + test('reads Uint64 value from eight bytes', () { final buffer = Uint8List.fromList([0, 0, 0, 1, 0, 0, 0, 0]); final reader = BinaryReader(buffer); expect(reader.readUint64(), equals(4294967296)); }); - test('reads Float32 correctly', () { + test('reads Float32 value from four bytes', () { final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); final reader = BinaryReader(buffer); expect(reader.readFloat32(), closeTo(3.1415927, 0.0000001)); }); - test('reads Float64 correctly', () { + test('reads Float64 value from eight bytes', () { final buffer = Uint8List.fromList([ 0x40, 0x09, @@ -67,7 +67,7 @@ void main() { ); }); - test('readBool correctly', () { + test('reads boolean values from single bytes', () { final buffer = Uint8List.fromList([0x01, 0x00]); final reader = BinaryReader(buffer); expect(reader.readBool(), isTrue); diff --git a/test/unit/binary_reader_edge_cases_test.dart b/test/unit/binary_reader_edge_cases_test.dart index ccbcc23..5973a1d 100644 --- a/test/unit/binary_reader_edge_cases_test.dart +++ b/test/unit/binary_reader_edge_cases_test.dart @@ -4,7 +4,7 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('BinaryReader Edge Cases', () { + group('BinaryReader Edge Cases and Validation', () { test('read beyond buffer throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); @@ -43,7 +43,7 @@ void main() { expect(reader.offset, equals(0)); }); - test('call() is an alias for readBytes', () { + test('call reads bytes and advances offset', () { final buffer = Uint8List.fromList([10, 20, 30, 40]); final reader = BinaryReader(buffer); expect(reader.call(2), equals([10, 20])); @@ -67,8 +67,8 @@ void main() { }); }); - group('Coverage edge cases', () { - test('peekByte returns byte without advancing', () { + group('Internal buffer access', () { + test('peekByte returns byte without advancing offset', () { final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); expect(reader.peekByte(), equals(0x42)); expect(reader.offset, equals(0)); diff --git a/test/unit/binary_reader_navigation_test.dart b/test/unit/binary_reader_navigation_test.dart index 64a9139..d2fb50b 100644 --- a/test/unit/binary_reader_navigation_test.dart +++ b/test/unit/binary_reader_navigation_test.dart @@ -5,14 +5,14 @@ import 'package:test/test.dart'; void main() { group('BinaryReader Navigation', () { - test('skip method correctly updates the offset', () { + test('skip advances offset by specified bytes', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03, 0x04]); final reader = BinaryReader(buffer)..skip(2); expect(reader.offset, equals(2)); expect(reader.readUint8(), equals(0x02)); }); - test('seek method sets position correctly', () { + test('seek sets offset to specified position', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer)..seek(2); expect(reader.offset, equals(2)); @@ -23,7 +23,7 @@ void main() { expect(reader.readUint8(), equals(1)); }); - test('rewind method moves back correctly', () { + test('rewind decreases offset without reading', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer) ..readBytes(3) // offset 3 @@ -41,7 +41,7 @@ void main() { }); group('baseOffset handling', () { - test('readBytes works correctly with non-zero baseOffset', () { + test('readBytes returns correct bytes with non-zero baseOffset', () { final largeBuffer = Uint8List.fromList(List.generate(100, (i) => i)); final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); final reader = BinaryReader(subBuffer); diff --git a/test/unit/binary_reader_string_test.dart b/test/unit/binary_reader_string_test.dart index 245b0df..5ca0c38 100644 --- a/test/unit/binary_reader_string_test.dart +++ b/test/unit/binary_reader_string_test.dart @@ -6,7 +6,7 @@ import 'package:test/test.dart'; void main() { group('BinaryReader String Operations', () { - test('readString correctly', () { + test('readString decodes UTF-8 bytes to original string', () { const str = 'Hello, world!'; final encoded = utf8.encode(str); final reader = BinaryReader(Uint8List.fromList(encoded)); diff --git a/test/unit/binary_writer_basic_test.dart b/test/unit/binary_writer_basic_test.dart index 3769a12..26a1345 100644 --- a/test/unit/binary_writer_basic_test.dart +++ b/test/unit/binary_writer_basic_test.dart @@ -14,12 +14,12 @@ void main() { expect(writer.takeBytes(), isEmpty); }); - test('writes single Uint8 value correctly', () { + test('writes single Uint8 value to buffer', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); }); - test('writes negative Int8 value correctly', () { + test('writes negative Int8 value as two\'s complement', () { writer.writeInt8(-1); expect(writer.takeBytes(), [255]); }); @@ -115,7 +115,7 @@ void main() { expect(writer.takeBytes(), equals([0x00])); }); - test('writes multiple boolean values correctly', () { + test('writes multiple boolean values to buffer', () { writer ..writeBool(true) ..writeBool(false) @@ -140,7 +140,7 @@ void main() { expect(reader.readBool(), isTrue); }); - test('updates bytesWritten correctly', () { + test('increments bytesWritten for each boolean write', () { expect(writer.bytesWritten, equals(0)); writer.writeBool(true); @@ -172,7 +172,7 @@ void main() { expect(writer.takeBytes(), [2]); }); - test('track bytesWritten correctly', () { + test('tracks bytesWritten for mixed type writes', () { writer.writeUint8(1); expect(writer.bytesWritten, equals(1)); diff --git a/test/unit/binary_writer_edge_cases_test.dart b/test/unit/binary_writer_edge_cases_test.dart index ff89ead..a1c91e6 100644 --- a/test/unit/binary_writer_edge_cases_test.dart +++ b/test/unit/binary_writer_edge_cases_test.dart @@ -35,17 +35,17 @@ void main() { }); group('Edge cases', () { - test('handle empty string correctly', () { + test('writes empty string with zero bytes', () { writer.writeString(''); expect(writer.bytesWritten, equals(0)); }); - test('handle empty byte array correctly', () { + test('writes empty byte array with zero bytes', () { writer.writeBytes([]); expect(writer.bytesWritten, equals(0)); }); - test('handle Float32 special values correctly', () { + test('writes Float32 NaN and infinity values', () { writer ..writeFloat32(double.nan) ..writeFloat32(double.infinity) diff --git a/test/unit/binary_writer_string_test.dart b/test/unit/binary_writer_string_test.dart index ba36b9e..37d95df 100644 --- a/test/unit/binary_writer_string_test.dart +++ b/test/unit/binary_writer_string_test.dart @@ -12,12 +12,12 @@ void main() { }); group('UTF-8 encoding', () { - test('encode ASCII characters correctly', () { + test('encode ASCII characters to matching bytes', () { writer.writeString('ABC123'); expect(writer.takeBytes(), equals([65, 66, 67, 49, 50, 51])); }); - test('encode Cyrillic characters correctly', () { + test('encode Cyrillic characters as multi-byte UTF-8', () { writer.writeString('Привет'); final bytes = writer.takeBytes(); @@ -25,7 +25,7 @@ void main() { expect(reader.readString(bytes.length), equals('Привет')); }); - test('encode Chinese characters correctly', () { + test('encode Chinese characters as multi-byte UTF-8', () { const str = '你好世界'; writer.writeString(str); final bytes = writer.takeBytes(); @@ -34,7 +34,7 @@ void main() { expect(reader.readString(bytes.length), equals(str)); }); - test('encode mixed Unicode string correctly', () { + test('encode mixed ASCII, Cyrillic, CJK, and emoji in single string', () { const str = 'Hello мир 世界 🌍'; writer.writeString(str); final bytes = writer.takeBytes(); From 7d169183ae78575c484d5fcbe0bd8f8d13dccaf0 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 17:55:07 +0300 Subject: [PATCH 13/17] cleanup --- test/unit/binary_writer_basic_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/binary_writer_basic_test.dart b/test/unit/binary_writer_basic_test.dart index 26a1345..c3c9ea2 100644 --- a/test/unit/binary_writer_basic_test.dart +++ b/test/unit/binary_writer_basic_test.dart @@ -19,7 +19,7 @@ void main() { expect(writer.takeBytes(), [1]); }); - test('writes negative Int8 value as two\'s complement', () { + test("writes negative Int8 value as two's complement", () { writer.writeInt8(-1); expect(writer.takeBytes(), [255]); }); From aaa4ad8e644364c806be76f77d197f45fb3bc344 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 18:09:24 +0300 Subject: [PATCH 14/17] update: topics --- pubspec.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 61be8c9..fec9197 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: 3.3.0 +version: 4.0.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues @@ -18,8 +18,10 @@ platforms: topics: - binary - - serialization - - deserialization + - bytes + - stream + - buffer + - protocol environment: sdk: ^3.10.0 @@ -28,6 +30,5 @@ dev_dependencies: benchmark_harness: ^2.4.0 pro_lints: ^6.0.0 test: ^1.31.1 -dependencies: - meta: ^1.18.2 + From 8c1f0f0f53940d2ca64ad4bc2a97d8e24f87a19d Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 18:49:24 +0300 Subject: [PATCH 15/17] doc: improvements --- lib/src/binary_reader.dart | 37 ++++++------ lib/src/binary_writer.dart | 10 ++-- lib/src/stream/binary_stream_transformer.dart | 1 + lib/src/stream/stream_binary_reader.dart | 59 +++++++++++++++++++ 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 5746708..869a51b 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -14,6 +14,7 @@ import 'dart:typed_data'; /// /// Example: /// ```dart +/// final bytes = Uint8List.fromList([0x00, 0x01, 0x02, 0x03]); /// final reader = BinaryReader(bytes); /// // Read various data types /// final id = reader.readUint32(); @@ -85,7 +86,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// ``` /// /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). - /// Asserts bounds in debug mode if attempting to read past buffer end. + /// Throws [RangeError] if attempting to read past buffer end. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readVarUint() { @@ -186,7 +187,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final version = reader.readUint8(); // Protocol version /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint8() { @@ -202,7 +203,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final offset = reader.readInt8(); // Small delta value /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt8() { @@ -220,7 +221,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final port = reader.readUint16(); // Network port number /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint16([Endian endian = .big]) { @@ -241,7 +242,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final temperature = reader.readInt16(); // -100 to 100°C /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt16([Endian endian = .big]) { @@ -262,7 +263,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final timestamp = reader.readUint32(); // Unix timestamp /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint32([Endian endian = .big]) { @@ -283,7 +284,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final coordinate = reader.readInt32(); // GPS coordinate /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt32([Endian endian = .big]) { @@ -309,7 +310,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final id = reader.readUint64(); // Large unique identifier /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint64([Endian endian = .big]) { @@ -332,7 +333,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final nanoseconds = reader.readInt64(); // High-precision time /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt64([Endian endian = .big]) { @@ -353,7 +354,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final temperature = reader.readFloat32(); // 25.5°C /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double readFloat32([Endian endian = .big]) { @@ -374,7 +375,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// final price = reader.readFloat64(); // $123.45 /// ``` /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double readFloat64([Endian endian = .big]) { @@ -401,7 +402,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// /// **Performance:** Zero-copy operation using buffer views. /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readBytes(int length) { @@ -456,7 +457,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// /// **Performance:** Zero-copy operation using buffer views. /// - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readVarBytes() { @@ -536,7 +537,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// ```dart /// final isActive = reader.readBool(); // Read active flag /// ``` - /// Asserts bounds in debug mode if insufficient bytes are available. + /// Throws [RangeError] if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') bool readBool() { @@ -578,7 +579,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// [offset] specifies where to start peeking (defaults to current position). /// /// Returns a view of the buffer without copying data. - /// Asserts bounds in debug mode if peeking past buffer end. + /// Throws [RangeError] if peeking past buffer end. /// /// Example: /// ```dart @@ -635,7 +636,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// This is useful for skipping over data you don't need to process. /// More efficient than reading and discarding data. /// - /// Asserts bounds in debug mode if skipping past buffer end. + /// Throws [RangeError] if skipping past buffer end. /// /// Example: /// ```dart @@ -660,7 +661,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// Sets the read position to the specified byte offset. /// /// This allows random access within the buffer. - /// Asserts bounds in debug mode if position is out of range. + /// Throws [RangeError] if position is out of range. /// /// Example: /// ```dart @@ -681,7 +682,7 @@ extension type BinaryReader._(_ReaderState _rs) { /// Moves the read position backwards by the specified number of bytes. /// /// This allows re-reading previously read data. - /// Asserts bounds in debug mode if rewinding before the start of the buffer. + /// Throws [RangeError] if rewinding before the start of the buffer. /// /// Example: /// ```dart diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 862677b..c3aa517 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -26,9 +26,7 @@ part 'string_utils.dart'; /// writer.writeFloat64(3.14); /// // Write length-prefixed string /// final text = 'Hello, World!'; -/// final utf8Bytes = utf8.encode(text); -/// writer.writeVarUint(utf8Bytes.length); -/// writer.writeString(text); +/// writer.writeVarString(text); /// // Extract bytes and optionally reuse writer /// final bytes = writer.takeBytes(); // Resets writer for reuse /// // or: final bytes = writer.toBytes(); // Keeps writer state @@ -448,7 +446,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// final text = 'Hello, 世界! 🌍'; /// final utf8Bytes = utf8.encode(text); /// writer.writeVarUint(utf8Bytes.length); // Write byte length - /// writer.writeString(text); // Write string data + /// writer.writeBytes(utf8Bytes); // Write pre-encoded string data /// // Or for simple fixed-length strings: /// writer.writeString('MAGIC'); // No length prefix needed /// ``` @@ -581,8 +579,8 @@ extension type BinaryWriter._(_WriterState _ws) { /// ``` /// This is equivalent to: /// ```dart - /// final utf8Bytes = utf8.encode(text); - /// writer.writeVarUint(utf8Bytes.length); + /// final byteLength = getUtf8Length(text); + /// writer.writeVarUint(byteLength); /// writer.writeString(text); /// ``` @pragma('vm:prefer-inline') diff --git a/lib/src/stream/binary_stream_transformer.dart b/lib/src/stream/binary_stream_transformer.dart index 365046c..12205cc 100644 --- a/lib/src/stream/binary_stream_transformer.dart +++ b/lib/src/stream/binary_stream_transformer.dart @@ -10,6 +10,7 @@ import 'stream_binary_reader.dart'; /// from the stream. /// /// To use it, extend this class and implement the [parse] method. +/// Return the parsed object, or `null` if there is not enough data yet. /// /// Example: /// ```dart diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index 95cba0b..3c4d87f 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -164,6 +164,8 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads an 8-bit unsigned integer (0-255). + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint8() { @@ -179,6 +181,8 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads an 8-bit signed integer (-128 to 127). + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt8() { @@ -195,6 +199,8 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { /// Reads a boolean value (1 byte). /// /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') bool readBool() { @@ -210,6 +216,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 16-bit unsigned integer. + /// + /// Throws [NotEnoughDataException] if fewer than 2 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint16([Endian endian = Endian.big]) { @@ -227,6 +237,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 16-bit signed integer. + /// + /// Throws [NotEnoughDataException] if fewer than 2 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt16([Endian endian = Endian.big]) { @@ -244,6 +258,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 32-bit unsigned integer. + /// + /// Throws [NotEnoughDataException] if fewer than 4 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint32([Endian endian = Endian.big]) { @@ -261,6 +279,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 32-bit signed integer. + /// + /// Throws [NotEnoughDataException] if fewer than 4 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt32([Endian endian = Endian.big]) { @@ -278,6 +300,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 64-bit unsigned integer. + /// + /// Throws [NotEnoughDataException] if fewer than 8 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readUint64([Endian endian = Endian.big]) { @@ -295,6 +321,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 64-bit signed integer. + /// + /// Throws [NotEnoughDataException] if fewer than 8 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readInt64([Endian endian = Endian.big]) { @@ -312,6 +342,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 32-bit floating-point number. + /// + /// Throws [NotEnoughDataException] if fewer than 4 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double readFloat32([Endian endian = Endian.big]) { @@ -329,6 +363,10 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a 64-bit floating-point number. + /// + /// Throws [NotEnoughDataException] if fewer than 8 bytes are available. + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') double readFloat64([Endian endian = Endian.big]) { @@ -354,6 +392,8 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads an unsigned variable-length integer (VarInt format). + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readVarUint() { @@ -383,6 +423,8 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a signed variable-length integer (ZigZag encoding). + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') int readVarInt() { @@ -391,6 +433,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a sequence of bytes. + /// + /// Throws [NotEnoughDataException] if fewer than [length] bytes are + /// available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readBytes(int length) { @@ -445,11 +490,16 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads all currently available bytes across all chunks. + /// + /// Throws [NotEnoughDataException] if no bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readRemainingBytes() => readBytes(_s.availableBytes); /// Reads a length-prefixed byte array. + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available for the + /// length prefix. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') Uint8List readVarBytes() { @@ -458,6 +508,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a UTF-8 encoded string of the specified byte length. + /// + /// Throws [NotEnoughDataException] if fewer than [length] bytes are + /// available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') String readString(int length, {bool allowMalformed = false}) { @@ -484,6 +537,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Reads a length-prefixed UTF-8 encoded string. + /// + /// Throws [NotEnoughDataException] if fewer than 1 byte is available for the + /// length prefix. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') String readVarString({bool allowMalformed = false}) { @@ -492,6 +548,9 @@ extension type StreamBinaryReader._(_StreamReaderState _s) { } /// Advances the read position by the specified number of bytes. + /// + /// Throws [NotEnoughDataException] if fewer than [length] bytes are + /// available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void skip(int length) { From 0fc1873c244a6ecc090e743873d83ede12f251f0 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 18:50:12 +0300 Subject: [PATCH 16/17] cleanup --- CONTRIBUTING.md | 116 ------------------------------------------------ 1 file changed, 116 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index a11a78c..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,116 +0,0 @@ -# Contributing to pro_binary - -Thank you for your interest in contributing! 🎉 - -## Getting Started - -1. Fork the repository -2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/pro_binary.git` -3. Create a branch: `git checkout -b feature/my-feature` -4. Install dependencies: `dart pub get` - -## Development - -### Running Tests - -```bash -# Run all tests (279+ tests) -dart test - -# Run specific test file -dart test test/binary_reader_test.dart -dart test test/binary_writer_test.dart -dart test test/integration_test.dart - -# Run with coverage -dart pub global activate coverage -dart pub global run coverage:test_with_coverage -``` - -### Test Organization - -Tests are organized as follows: - -- **binary_reader_test.dart**: Unit tests for BinaryReader (190+ tests) - - Read operations for all data types - - Boundary conditions and edge cases - - UTF-8 encoding with special characters - - Malformed sequence handling - -- **binary_writer_test.dart**: Unit tests for BinaryWriter (200+ tests) - - Write operations for all data types - - Buffer management and expansion - - Input validation and range checks - - Float precision and special values - -- **integration_test.dart**: Integration tests (60+ tests) - - Complete read-write cycles - - Round-trip validation - - Complex data structures - - String handling (ASCII, Cyrillic, Chinese, emoji) - - Large data operations - - Stress tests with nested structures - -- **Performance tests**: Benchmark measurements - - binary_reader_performance_test.dart - - binary_writer_performance_test.dart - -### Code Style - -```bash -# Format code -dart format . - -# Analyze code -dart analyze - -# Fix common issues -dart fix --apply -``` - -### Before Submitting - -- [ ] All tests pass (`dart test`) -- [ ] Code is formatted (`dart format .`) -- [ ] No analysis issues (`dart analyze`) -- [ ] Added tests for new features -- [ ] Updated CHANGELOG.md -- [ ] Updated documentation if needed - -## Pull Request Process - -1. Update the README.md with details of changes if applicable -2. Update the CHANGELOG.md with a note describing your changes -3. Ensure all tests pass and code is properly formatted -4. Submit a pull request with a clear description of changes - -## Reporting Bugs - -Use the [Bug Report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - -- Clear description of the issue -- Steps to reproduce -- Expected vs actual behavior -- Code sample -- Environment details - -## Suggesting Features - -Use the [Feature Request template](.github/ISSUE_TEMPLATE/feature_request.md) and describe: - -- The feature you'd like -- Your use case -- Proposed API (if applicable) -- Alternative solutions considered - -## Code of Conduct - -- Be respectful and inclusive -- Provide constructive feedback -- Focus on what is best for the community - -## Questions? - -Feel free to open a [Discussion](https://github.com/pro100andrey/pro_binary/discussions) or reach out to maintainers. - -Thank you for contributing! 🚀 From fd45902d0181e2285edc262ea5345bfd41a1652f Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 26 May 2026 18:50:52 +0300 Subject: [PATCH 17/17] bump version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 643774e..a79913f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ```yaml dependencies: - pro_binary: ^3.3.0 + pro_binary: ^4.0.0 ``` ## Quick Start