diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab8259c..b2e1523 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,13 +39,13 @@ jobs: run: dart analyze --fatal-infos - name: Run tests - run: dart test + run: dart test -x benchmark - name: Run tests with coverage if: matrix.os == 'ubuntu-latest' && matrix.sdk == 'stable' run: | dart pub global activate coverage - dart pub global run coverage:test_with_coverage + dart pub global run coverage:test_with_coverage -- -x benchmark - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.sdk == 'stable' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9aeef..dbaf15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 3.0.0 + +**Improvements:** + +- **feat**: New methods and properties + - `BinaryWriterPool` for reusing `BinaryWriter` instances + - `getUtf8Length(String)` to calculate UTF-8 byte length without encoding + - `writeVarString(String)` and `readVarString()` for variable-length string encoding + - `writeBool` and `readBool` methods for boolean values + - `writeVarUint` and `readVarUint` for variable-length unsigned integers + - `writeVarInt` and `readVarInt` for variable-length signed integers + - `writeVarBytes` and `readVarBytes` for variable-length byte arrays + - Navigation methods in `BinaryReader`: `peekBytes()`, `skip()`, `seek()`, `rewind()`, and `reset()` +- **docs**: Comprehensive documentation overhaul + - Added detailed API documentation with usage examples for all methods + - Documented `writeVarString()`, `readVarString()`, and `getUtf8Length()` + - Included performance notes and best practices + - Added inline comments explaining complex encoding algorithms +- **test**: Expanded test suite + - Coverage for all new methods and edge cases + - Performance benchmarks for encoding/decoding functions + - Validation tests for UTF-8 handling and error scenarios +- **improvement**: Refactored internal codebase + - Improved modularity and readability + - Enhanced error handling with descriptive messages + - Optimized buffer management for better performance + +- **fix**: Resolved known issues + ## 2.2.0 **test**: Added integration tests for new error handling features diff --git a/README.md b/README.md index 88fb810..c019d46 100644 --- a/README.md +++ b/README.md @@ -4,171 +4,559 @@ [![Tests](https://github.com/pro100andrey/pro_binary/workflows/Tests/badge.svg)](https://github.com/pro100andrey/pro_binary/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -Efficient binary serialization library for Dart with comprehensive boundary checks and detailed error messages. +High-performance binary serialization library for Dart with zero-copy operations, efficient memory management, and Protocol Buffers-compatible VarInt encoding. ## Features -- ✅ Read/write operations for all primitive types (int8/16/32/64, uint8/16/32/64, float32/64) -- ✅ Big-endian and little-endian support -- ✅ Comprehensive boundary checks with detailed error messages -- ✅ UTF-8 string encoding with multibyte character support -- ✅ Dynamic buffer resizing with efficient memory management -- ✅ Zero-copy operations where possible +- 🚀 **Zero-copy reads**: Direct `Uint8List` views without data duplication +- ⚡ **Optimized writes**: Exponential buffer growth strategy (×1.5) with pooling support +- 🔢 **VarInt encoding**: Protocol Buffers-compatible variable-length integer encoding +- 🎯 **Type-safe API**: Full support for all Dart primitive types (int8-int64, float32/64, bool) +- 🌐 **Endianness support**: Both big-endian (default) and little-endian byte order +- 📦 **Memory efficient**: Automatic buffer management with configurable initial capacity +- 🧪 **Battle-tested**: 556+ tests with extensive edge case coverage ## Installation -Add this to your package's `pubspec.yaml` file: - -``` yaml +```yaml dependencies: - pro_binary: ^2.1.0 + pro_binary: ^3.0.0 ``` -Then, run `pub get` to install the package. - ## Quick Start -### Writing - ```dart import 'package:pro_binary/pro_binary.dart'; -void main() { - final writer = BinaryWriter() - ..writeUint8(42) - ..writeUint32(1000000, Endian.little) - ..writeFloat64(3.14159) - ..writeString('Hello'); +// Writing data +final writer = BinaryWriter(); +writer.writeUint32(42); +writer.writeVarString('Hello, World!'); // Length-prefixed +final bytes = writer.takeBytes(); + +// Reading data +final reader = BinaryReader(bytes); +final number = reader.readUint32(); // 42 +final text = reader.readVarString(); // 'Hello, World!' +``` + +## Core API + +### Writing Data + +```dart +final writer = BinaryWriter(); + +// Integers (8, 16, 32, 64-bit signed/unsigned) +writer.writeUint8(255); +writer.writeInt32(-1000, .little); +writer.writeUint64(9999999); + +// Floats +writer.writeFloat32(3.14); +writer.writeFloat64(3.14159265359); + +// Variable-length integers (space-efficient!) +writer.writeVarUint(42); // Unsigned VarInt +writer.writeVarInt(-42); // Signed VarInt with ZigZag + +// Binary data +writer.writeBytes([1, 2, 3]); // Raw bytes +writer.writeVarBytes([1, 2, 3]); // Length-prefixed bytes + +// Strings +writer.writeString('text'); // Raw UTF-8 string (no length prefix) +writer.writeVarString('Hello'); // Length-prefixed UTF-8 string + +// Boolean +writer.writeBool(true); // Single byte (0x01 or 0x00) + +// Get result +final bytes = writer.takeBytes(); // Gets bytes and resets +// or +final view = writer.toBytes(); // Gets bytes, keeps state +``` - final bytes = writer.takeBytes(); - print('Written ${bytes.length} bytes'); +### Reading Data + +```dart +final reader = BinaryReader(bytes); + +// Read primitives (matching write order) +final u8 = reader.readUint8(); +final i32 = reader.readInt32(.little); +final f64 = reader.readFloat64(); + +// Variable-length integers +final count = reader.readVarUint(); +final delta = reader.readVarInt(); + +// Binary data +final data = reader.readBytes(10); // Read 10 bytes +final varData = reader.readVarBytes(); // Read length-prefixed bytes +final remaining = reader.readRemainingBytes(); // Read all remaining + +// Strings +final text = reader.readString(10); // Read 10 UTF-8 bytes +final message = reader.readVarString(); // Read length-prefixed string + +// Boolean +final flag = reader.readBool(); // Read boolean (0x00 = false, other = true) + +// Navigation +reader.skip(4); // Skip bytes +reader.seek(10); // Jump to position 10 +reader.rewind(2); // Move back 2 bytes +final peek = reader.peekBytes(2); // Look ahead without consuming +reader.reset(); // Go back to start + +// Check state +print(reader.offset); // Current position +print(reader.length); // Total buffer size +print(reader.availableBytes); // Bytes left to read +if (reader.hasBytes(4)) { // Check if enough bytes available + final value = reader.readUint32(); } ``` -### Reading +## Real-World Examples + +### Protocol Messages ```dart -import 'dart:typed_data'; -import 'package:pro_binary/pro_binary.dart'; +// Encode message +final writer = BinaryWriter(); +writer.writeUint8(0x42); // Message type +writer.writeVarBytes(payload); // Length-prefixed payload +sendToServer(writer.takeBytes()); + +// Decode message +final reader = BinaryReader(received); +final type = reader.readUint8(); +final payload = reader.readVarBytes(); // Reads length + data +``` -void main() { - final data = Uint8List.fromList([42, 64, 66, 15, 0]); - final reader = BinaryReader(data); +### Length-Prefixed Strings - final value1 = reader.readUint8(); // 42 - final value2 = reader.readUint32(Endian.little); // 1000000 - - print('Read: $value1, $value2'); - print('Remaining: ${reader.availableBytes} bytes'); +```dart +// Recommended: Use writeVarString (automatic length) +writer.writeVarString('Hello, 世界! 🌍'); + +// Or manually (equivalent to above): +final text = 'Hello, 世界! 🌍'; +final utf8Length = getUtf8Length(text); // Calculate UTF-8 byte length +writer.writeVarUint(utf8Length); +writer.writeString(text); + +// Reading: Use readVarString (reads length + data) +final text = reader.readVarString(); + +// Or manually (equivalent to above): +final length = reader.readVarUint(); +final text = reader.readString(length); +``` + +### Struct-like Data + +```dart +class Player { + final int id; + final String name; + final double x, y; + + Player(this.id, this.name, this.x, this.y); + + void writeTo(BinaryWriter w) { + w.writeUint32(id); + w.writeVarString(name); // Length-prefixed string + w.writeFloat64(x); + w.writeFloat64(y); + } + + static Player readFrom(BinaryReader r) { + final id = r.readUint32(); + final name = r.readVarString(); // Reads length + string + final x = r.readFloat64(); + final y = r.readFloat64(); + return Player(id, name, x, y); + } } ``` -## API Overview +## Architecture ### BinaryWriter ```dart -final writer = BinaryWriter(initialBufferSize: 64); - -// Write operations -writer.writeUint8(255); -writer.writeInt8(-128); -writer.writeUint16(65535, Endian.big); -writer.writeInt16(-32768, Endian.big); -writer.writeUint32(4294967295, Endian.big); -writer.writeInt32(-1000, Endian.big); -writer.writeUint64(9223372036854775807, Endian.big); -writer.writeInt64(-9223372036854775808, Endian.big); -writer.writeFloat32(3.14, Endian.big); -writer.writeFloat64(3.14159, Endian.big); -writer.writeBytes([1, 2, 3]); -writer.writeString('text'); - -// Buffer operations -final bytes = writer.toBytes(); // Get view without reset -final result = writer.takeBytes(); // Get view and reset -writer.reset(); // Reset without returning -print(writer.bytesWritten); // Check written size +final writer = BinaryWriter(initialBufferSize: 128); // Default: 128 bytes ``` +**Buffer Management:** + +- Initial capacity: 128 bytes (configurable) +- Growth strategy: `newCapacity = ((currentCapacity * 1.5).ceil() + 63) & ~63` (1.5× + 64-byte alignment) +- Minimum expansion: Ensures space for requested bytes +- Resets buffer without reallocation: `writer.reset()` +- Takes ownership of buffer: `writer.takeBytes()` (one-time use, resets writer) +- Creates view without reset: `writer.toBytes()` (reusable) + +**Write Operations:** + +- Fixed-width integers: `writeUint8`, `writeInt16`, `writeUint32`, `writeInt64`, etc. +- Variable-length integers: `writeVarUint` (unsigned), `writeVarInt` (ZigZag-encoded signed) +- Floating-point: `writeFloat32`, `writeFloat64` +- Binary data: `writeBytes`, `writeVarBytes` (length-prefixed) +- Strings: `writeString` (raw UTF-8), `writeVarString` (length-prefixed) +- Boolean: `writeBool` (1 byte: 0x00 or 0x01) + +**Helper Functions:** + +- `getUtf8Length(String)`: Calculate UTF-8 byte length without encoding + ### BinaryReader ```dart -final reader = BinaryReader(buffer); +final reader = BinaryReader(bytes); +``` -// Read operations -final u8 = reader.readUint8(); -final i8 = reader.readInt8(); -final u16 = reader.readUint16(Endian.big); -final i16 = reader.readInt16(Endian.big); -final u32 = reader.readUint32(Endian.big); -final i32 = reader.readInt32(Endian.little); -final u64 = reader.readUint64(Endian.big); -final i64 = reader.readInt64(Endian.big); -final f32 = reader.readFloat32(Endian.big); -final f64 = reader.readFloat64(Endian.big); -final bytes = reader.readBytes(10); -final text = reader.readString(5); - -// Peek without advancing position -final peeked = reader.peekBytes(4); // View without consuming +**Zero-Copy Design:** -// Navigation -reader.skip(4); // Skip bytes -final pos = reader.offset; // Current position -final used = reader.usedBytes; // Bytes read so far -reader.reset(); // Reset to start -print(reader.availableBytes); // Remaining bytes +- No buffer copying: operates on `Uint8List.view` of input data +- Direct memory access via `ByteData` for endianness handling +- Automatic offset tracking with bounds checking + +**Read Operations:** + +- Fixed-width integers: `readUint8`, `readInt16`, `readUint32`, `readInt64`, etc. +- Variable-length integers: `readVarUint`, `readVarInt` (ZigZag-decoded) +- Floating-point: `readFloat32`, `readFloat64` +- Binary data: `readBytes`, `readVarBytes`, `readRemainingBytes` +- Strings: `readString` (raw UTF-8), `readVarString` (length-prefixed) +- Boolean: `readBool` + +**Navigation API:** + +- `skip(int bytes)`: Move forward by N bytes +- `seek(int position)`: Jump to absolute position +- `rewind(int bytes)`: Move backward by N bytes +- `reset()`: Return to start +- `peekBytes(int length, [int offset])`: Look ahead without consuming +- `hasBytes(int count)`: Check if enough bytes available + +**State Inspection:** + +- `offset`: Current read position (0-based) +- `length`: Total buffer size +- `availableBytes`: Remaining unread bytes + +## VarInt Encoding + +VarInt uses fewer bytes for small numbers: + +```dart +writer.writeVarUint(42); // 1 byte (vs 4 for Uint32) +writer.writeVarUint(300); // 2 bytes +writer.writeVarUint(1000000); // 3 bytes + +writer.writeVarInt(-1); // 1 byte (ZigZag encoded) +writer.writeVarInt(-1000); // 2 bytes +``` + +**Implementation Details:** + +- Protocol Buffers Base 128 Varint encoding +- 7 data bits + 1 continuation bit per byte +- Maximum 10 bytes for 64-bit values +- ZigZag encoding for signed integers: `(n << 1) ^ (n >> 63)` +- Fast path optimization for single-byte values (0-127) + +**Use VarUint** for: lengths, counts, IDs +**Use VarInt** for: deltas, offsets, signed values + +## Encoding Efficiency + +VarInt encoding significantly reduces payload size for small values: + +| Value | VarInt | Fixed Uint32 | Savings | +| ------- | -------- | -------------- | --------- | +| 0 | 1 byte | 4 bytes | **75%** | +| 42 | 1 byte | 4 bytes | **75%** | +| 127 | 1 byte | 4 bytes | **75%** | +| 128 | 2 bytes | 4 bytes | **50%** | +| 300 | 2 bytes | 4 bytes | **50%** | +| 16,384 | 3 bytes | 4 bytes | **25%** | +| 1,000,000 | 3 bytes | 4 bytes | **25%** | +| 268,435,455 | 4 bytes | 4 bytes | **0%** | + +**Use VarInt for:** lengths, counts, sizes, small IDs +**Use fixed-width for:** timestamps, coordinates, fixed-size IDs + +## Tips & Best Practices + +### Performance Optimization + +**Pre-allocate buffers** for known data sizes: + +```dart +// For ~1KB messages +final writer = BinaryWriter(initialBufferSize: 1024); + +// Avoid multiple small allocations +final writer = BinaryWriter(initialBufferSize: 8192); // For bulk writes ``` -## Error Handling +**Use object pooling** for high-frequency operations: + +```dart +// Acquire from pool (default 1KB buffer) +final writer = BinaryWriterPool.acquire(); +try { + writer.writeUint32(value); + final bytes = writer.toBytes(); // Get bytes, keep writer alive + send(bytes); +} finally { + // Return to pool for reuse (max 32 writers, max 64KB buffers) + BinaryWriterPool.release(writer); +} + +// Pool statistics +final stats = BinaryWriterPool.stats; +print('Pooled writers: ${stats.pooled}'); // Current pool size +print('Max pool size: ${stats.maxPoolSize}'); // Maximum capacity (32) +print('Peak pool size: ${stats.peakPoolSize}'); // High water mark +print('Acquire hits: ${stats.acquireHit}'); // Successful reuses +print('Acquire misses: ${stats.acquireMiss}'); // New allocations +print('Hit rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%'); // Cache efficiency +print('Discarded: ${stats.discardedLargeBuffers}'); // Oversized buffers + +// Clear pool manually +BinaryWriterPool.clear(); +``` -All read operations validate boundaries and provide detailed error messages: +**Choose correct integer type**: ```dart +// VarInt for small values (lengths, counts) +writer.writeVarUint(items.length); // 1 byte for length < 128 + +// Fixed-width for large/unpredictable values +writer.writeUint32(timestamp); // Always 4 bytes, predictable +writer.writeUint64(uuid); // Fixed 8 bytes +``` + +### Endianness + +Default: **big-endian** (network byte order). Specify when needed: + +```dart +// Explicit endianness +writer.writeUint32(value, .little); +writer.writeFloat64(3.14, .big); + +// Reading must match writing +final value = reader.readUint32(.little); +``` + +**When to use little-endian:** + +- Interop with x86/ARM systems (native byte order) +- Matching existing binary formats (e.g., RIFF, BMP) +- Performance-critical code on little-endian CPUs + +### String Encoding + +Always use **length-prefixed** encoding for variable-length strings: + +```dart +// ✅ Good: Self-describing +writer.writeVarString('Hello'); +// Equivalent to: +// writer.writeVarUint(utf8.encode('Hello').length); +// writer.writeString('Hello'); + +// ❌ Bad: No way to determine string boundaries +writer.writeString('Hello'); +writer.writeString('World'); // Where does first string end? +``` + +For **fixed-length** strings, calculate UTF-8 byte length: + +```dart +final text = 'Hello, 世界!'; +final utf8Length = getUtf8Length(text); // Calculate without encoding +writer.writeUint16(utf8Length); // Store byte length +writer.writeString(text); + +// Reading +final byteLength = reader.readUint16(); +final text = reader.readString(byteLength); + +// Handle malformed UTF-8 sequences +final strict = reader.readString(10, allowMalformed: false); // Throws on error +final lenient = reader.readString(10); // allowMalformed: true (default) - uses � +``` + +### Error Handling + +All operations throw `RangeError` on invalid data or buffer overflow: + +```dart +// Buffer underflow try { - reader.readUint32(); // Not enough bytes -} catch (e) { - // AssertionError: Not enough bytes to read Uint32: - // required 4 bytes, available 2 bytes at offset 10 + final value = reader.readUint32(); // Not enough bytes +} on RangeError catch (e) { + print('Buffer underflow: $e'); +} + +// Invalid VarInt +try { + final value = reader.readVarInt(); // Malformed encoding +} on FormatException catch (e) { + print('Invalid VarInt: $e'); +} + +// String decoding errors +try { + final text = reader.readString(10, allowMalformed: false); +} on FormatException catch (e) { + print('Invalid UTF-8: $e'); } ``` -## Contributing +### Design Patterns -Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on: +**Tagged unions** (discriminated unions): -- How to set up the development environment -- Running tests and coverage -- Code style and formatting -- Submitting pull requests +```dart +enum MessageType { ping, data, ack } + +void writeMessage(BinaryWriter w, MessageType type, dynamic payload) { + w.writeUint8(type.index); + switch (type) { + case MessageType.ping: + // No payload + break; + case MessageType.data: + w.writeVarBytes(payload as List); // Length-prefixed + break; + case MessageType.ack: + w.writeUint32(payload as int); // Sequence number + break; + } +} + +void readMessage(BinaryReader r) { + final type = MessageType.values[r.readUint8()]; + switch (type) { + case MessageType.ping: + // No payload + break; + case MessageType.data: + final payload = r.readVarBytes(); // Reads length + data + break; + case MessageType.ack: + final seqNum = r.readUint32(); + break; + } +} +``` -For bugs and features, use the [issue templates](https://github.com/pro100andrey/pro_binary/issues/new/choose). +**Version-tolerant serialization**: + +```dart +class Message { + static const int version = 2; + + void writeTo(BinaryWriter w) { + w.writeUint8(version); // Version byte + w.writeVarUint(id); // Field 1 + w.writeVarString(text); // Field 2 + // Version 2: added timestamp + if (version >= 2) { + w.writeUint64(timestamp); + } + } + + static Message readFrom(BinaryReader r) { + final ver = r.readUint8(); + final id = r.readVarUint(); + final text = r.readVarString(); + final timestamp = ver >= 2 ? r.readUint64() : 0; + return Message(id, text, timestamp); + } +} +``` ## Testing -The library includes comprehensive test coverage with **279+ tests** covering: +Comprehensive test suite with **556 tests** covering: + +- ✅ **Unit tests (417)**: Isolated BinaryReader/Writer method testing + - All primitive types (int8-int64, float32/64, bool) + - VarInt/VarUint encoding/decoding (70+ dedicated tests) + - Boundary conditions and overflow detection + - UTF-8 handling (multi-byte chars, emojis, malformed sequences) + - Navigation API (seek, skip, rewind, peek) + - Error handling and exception cases -- **Basic operations**: All read/write methods for each data type -- **Endianness**: Big-endian and little-endian operations -- **Edge cases**: Boundary conditions, overflow, special values (NaN, Infinity) -- **UTF-8 handling**: Multi-byte characters, emoji, malformed sequences -- **Buffer management**: Expansion, growth strategy, memory efficiency -- **Integration tests**: Complete read-write cycles and round-trip validation -- **Performance tests**: Benchmark measurements for optimization +- ✅ **Integration tests (92)**: End-to-end roundtrip validation + - Write → Read consistency for all data types + - Buffer expansion under load + - Complex data structure serialization -Run tests with: +- ✅ **Performance benchmarks (51)**: Optimization tracking + - Read/write throughput for all operations + - Buffer growth patterns + - VarInt encoding efficiency by value range + - Navigation operation costs + +Run tests: ```bash +# Run unit + integration tests (skip benchmarks) +dart test -x benchmark + +# Run performance benchmarks only +dart test -t benchmark + +# Run all tests including benchmarks dart test -``` -Analyze code quality: +# Run specific test file +dart test test/unit/binary_reader_test.dart -```bash -dart analyze +# Run with coverage +dart pub global activate coverage +dart pub global run coverage:test_with_coverage -- -x benchmark + +# Code analysis +dart analyze --fatal-infos +dart format --set-exit-if-changed . ``` +## Contributing + +Contributions are welcome! Please: + +1. **Open an issue** first to discuss major changes +2. **Follow existing code style** (run `dart format`) +3. **Add tests** for new features (maintain >95% coverage) +4. **Update documentation** including README examples +5. **Run full test suite** before submitting PR + + ```bash + dart analyze --fatal-infos + dart format --set-exit-if-changed . + dart test + ``` + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines. + ## License -This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details. +MIT License - see [LICENSE](./LICENSE) for details. + +--- + +Need help? Found a bug? Have a feature request? +👉 [Open an issue](https://github.com/pro100andrey/pro_binary/issues) diff --git a/benchmark_baseline.json b/benchmark_baseline.json new file mode 100644 index 0000000..27de111 --- /dev/null +++ b/benchmark_baseline.json @@ -0,0 +1,14 @@ +{ + "schema": 1, + "createdAt": "2025-12-29T13:09:01.837273Z", + "dart": "Dart SDK version: 3.10.4 (stable) (Tue Dec 9 00:01:55 2025 -0800) on \"linux_x64\"", + "runs": 10, + "warmup": 2, + "tag": "benchmark", + "benchmarkRegex": null, + "mediansUs": { + "BinaryReader performance test": 3706.0389090909093, + "BinaryWriter performance test": 150.3888255564516, + "GetStringLength performance test": 6697.887155444721 + } +} \ No newline at end of file diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..6c78dc3 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,7 @@ +# Test configuration for package:test / dart test. +# +# Declare custom tags to avoid warnings and to document intent. + +tags: + benchmark: + description: Performance/benchmark tests (excluded from CI by default). diff --git a/example/main.dart b/example/main.dart index 8919557..08ade16 100644 --- a/example/main.dart +++ b/example/main.dart @@ -17,7 +17,7 @@ void writeExample() { final writer = BinaryWriter() ..writeUint8(42) - ..writeInt32(-1000, Endian.little) + ..writeInt32(-1000, .little) ..writeFloat64(3.14159) ..writeString('Hello, World!'); @@ -37,7 +37,7 @@ void readExample() { final reader = BinaryReader(buffer); print('uint8: ${reader.readUint8()}'); - print('int32: ${reader.readInt32(Endian.little)}'); + print('int32: ${reader.readInt32(.little)}'); print('float64: ${reader.readFloat64()}'); print('string: ${reader.readString(5)}'); print('Position: ${reader.offset}/${buffer.length}\n'); diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index f692cff..0d884a7 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -1,188 +1,445 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'binary_reader_interface.dart'; - -/// A high-performance implementation of [BinaryReaderInterface] for decoding -/// binary data. +/// A high-performance binary reader for decoding data from a byte buffer. +/// +/// Provides methods for reading various data types including: +/// - Variable-length integers (VarInt, ZigZag) +/// - Fixed-width integers (8, 16, 32, 64-bit signed and unsigned) +/// - Floating-point numbers (32 and 64-bit) +/// - Byte arrays and strings +/// +/// The reader maintains an internal offset that advances as data is read. +/// Use [reset] to restart reading from the beginning. /// /// Example: /// ```dart -/// final bytes = Uint8List.fromList([0, 0, 0, 42]); /// final reader = BinaryReader(bytes); -/// final value = reader.readUint32(); // 42 -/// print(reader.availableBytes); // 0 +/// // Read various data types +/// final id = reader.readUint32(); +/// final value = reader.readFloat64(); +/// // Read length-prefixed string +/// final stringLength = reader.readVarUint(); +/// final text = reader.readString(stringLength); +/// // Check remaining data +/// print('Bytes left: ${reader.availableBytes}'); /// ``` -class BinaryReader extends BinaryReaderInterface { - /// Creates a new [BinaryReader] for the given byte buffer. +extension type const BinaryReader._(_ReaderState _rs) { + /// Creates a new [BinaryReader] from the given byte buffer. /// - /// The [buffer] parameter must be a [Uint8List] containing the data to read. - /// The reader starts at position 0 and can read up to the buffer's length. - BinaryReader(Uint8List buffer) - : _buffer = buffer, - _data = ByteData.sublistView(buffer), - _length = buffer.length; + /// The reader will start at position 0 and can read up to `buffer.length` + /// bytes. + BinaryReader(Uint8List buffer) : this._(_ReaderState(buffer)); - /// The underlying byte buffer being read from. - final Uint8List _buffer; - - /// Efficient view for typed data access. - final ByteData _data; + /// Returns the number of bytes remaining to be read. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int get availableBytes => _rs.length - _rs.offset; - /// Total length of the buffer. - final int _length; + /// Returns the current read position in the buffer. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int get offset => _rs.offset; - /// Current read position in the buffer. - var _offset = 0; + /// Returns the total length of the buffer in bytes. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int get length => _rs.length; - /// Performs inline bounds check to ensure safe reads. + /// Reads an unsigned variable-length integer encoded using VarInt format. /// - /// Throws [AssertionError] if attempting to read beyond buffer boundaries. + /// VarInt encoding uses the lower 7 bits of each byte for data and the + /// highest bit as a continuation flag. This format is space-efficient + /// for small unsigned numbers (1-5 bytes for typical 32-bit values). + /// + /// The algorithm: + /// 1. Read a byte and extract the lower 7 bits + /// 2. If the 8th bit is set, continue reading + /// 3. Shift and combine all 7-bit chunks + /// + /// **Use this for:** Lengths, counts, sizes, unsigned IDs. + /// + /// For signed integers (especially with negative values), use [readVarInt] + /// which uses ZigZag decoding for better compression of negative numbers. + /// + /// Example: + /// ```dart + /// final count = reader.readVarUint(); // Read array length + /// for (var i = 0; i < count; i++) { + /// // Process array elements + /// } + /// ``` + /// + /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). + /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _checkBounds(int bytes, String type, [int? offset]) { - assert( - (offset ?? _offset) + bytes <= _length, - 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_length - _offset} bytes at offset $_offset', - ); - } + int readVarUint() { + final list = _rs.list; + final len = _rs.length; + var offset = _rs.offset; + + if (offset >= len) { + throw RangeError('VarInt out of bounds: offset=$offset length=$len'); + } + + // Fast path: single byte (0-127) — most common case + var byte = list[offset++]; + if ((byte & 0x80) == 0) { + _rs.offset = offset; + return byte; + } - @override - int get availableBytes => _length - _offset; + // Multi-byte VarInt (optimized for 2-3 byte case) + var result = byte & 0x7f; + var shift = 7; - @override - int get usedBytes => _offset; + // Process remaining bytes: up to 9 more (total 10 max) + for (var i = 1; i < 10; i++) { + if (offset >= len) { + throw RangeError( + 'VarInt out of bounds: offset=$offset length=$len (truncated)', + ); + } + byte = list[offset++]; + result |= (byte & 0x7f) << shift; + + if ((byte & 0x80) == 0) { + _rs.offset = offset; + return result; + } + + shift += 7; + } + + throw const FormatException('VarInt is too long (more than 10 bytes)'); + } + + /// Reads a signed variable-length integer using ZigZag decoding. + /// + /// ZigZag encoding maps signed integers to unsigned values such that + /// small absolute values (both positive and negative) use fewer bytes: + /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. + /// + /// First reads an unsigned VarInt, then applies ZigZag decoding. + /// Decoding formula: (n >>> 1) ^ -(n & 1) + /// This reverses the encoding: (n << 1) ^ (n >> 63) + /// + /// **Use this for:** Signed values, deltas, offsets, coordinates. + /// + /// Example: + /// ```dart + /// final delta = reader.readVarInt(); // Can be positive or negative + /// final position = lastPosition + delta; + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int readVarInt() { + final v = readVarUint(); + // Decode: right shift by 1, XOR with sign-extended LSB + return (v >>> 1) ^ -(v & 1); + } + + /// Reads an 8-bit unsigned integer (0-255). + /// + /// Example: + /// ```dart + /// final version = reader.readUint8(); // Protocol version + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override int readUint8() { _checkBounds(1, 'Uint8'); - return _data.getUint8(_offset++); + + return _rs.data.getUint8(_rs.offset++); } + /// Reads an 8-bit signed integer (-128 to 127). + /// + /// Example: + /// ```dart + /// final offset = reader.readInt8(); // Small delta value + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override int readInt8() { _checkBounds(1, 'Int8'); - return _data.getInt8(_offset++); + return _rs.data.getInt8(_rs.offset++); } + /// Reads a 16-bit unsigned integer (0-65535). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final port = reader.readUint16(); // Network port number + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readUint16([Endian endian = Endian.big]) { + int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); - final value = _data.getUint16(_offset, endian); - _offset += 2; + final value = _rs.data.getUint16(_rs.offset, endian); + _rs.offset += 2; return value; } + /// Reads a 16-bit signed integer (-32768 to 32767). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final temperature = reader.readInt16(); // -100 to 100°C + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readInt16([Endian endian = Endian.big]) { + int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); - final value = _data.getInt16(_offset, endian); - _offset += 2; + final value = _rs.data.getInt16(_rs.offset, endian); + _rs.offset += 2; return value; } + /// Reads a 32-bit unsigned integer (0 to 4,294,967,295). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final timestamp = reader.readUint32(); // Unix timestamp + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readUint32([Endian endian = Endian.big]) { + int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); - final value = _data.getUint32(_offset, endian); - _offset += 4; - + final value = _rs.data.getUint32(_rs.offset, endian); + _rs.offset += 4; return value; } + /// Reads a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final coordinate = reader.readInt32(); // GPS coordinate + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readInt32([Endian endian = Endian.big]) { + int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); - - final value = _data.getInt32(_offset, endian); - _offset += 4; - + final value = _rs.data.getInt32(_rs.offset, endian); + _rs.offset += 4; return value; } + /// Reads a 64-bit unsigned integer. + /// + /// **Note:** Since Dart's `int` type is a signed 64-bit integer, this method + /// will return negative values for numbers greater than 2^63 - 1. + /// + /// On web targets, precision is limited to 2^53. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final id = reader.readUint64(); // Large unique identifier + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readUint64([Endian endian = Endian.big]) { + int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); - - final value = _data.getUint64(_offset, endian); - _offset += 8; - + final value = _rs.data.getUint64(_rs.offset, endian); + _rs.offset += 8; return value; } + /// Reads a 64-bit signed integer. + /// + /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final nanoseconds = reader.readInt64(); // High-precision time + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - int readInt64([Endian endian = Endian.big]) { + int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); - - final value = _data.getInt64(_offset, endian); - _offset += 8; - + final value = _rs.data.getInt64(_rs.offset, endian); + _rs.offset += 8; return value; } + /// Reads a 32-bit floating-point number (IEEE 754 single precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final temperature = reader.readFloat32(); // 25.5°C + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - double readFloat32([Endian endian = Endian.big]) { + double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); - final value = _data.getFloat32(_offset, endian); - _offset += 4; + final value = _rs.data.getFloat32(_rs.offset, endian); + _rs.offset += 4; return value; } + /// Reads a 64-bit floating-point number (IEEE 754 double precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final price = reader.readFloat64(); // $123.45 + /// ``` + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - double readFloat64([Endian endian = Endian.big]) { + double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); - final value = _data.getFloat64(_offset, endian); - _offset += 8; - + final value = _rs.data.getFloat64(_rs.offset, endian); + _rs.offset += 8; return value; } + /// Reads a sequence of bytes and returns them as a [Uint8List]. + /// + /// Returns a view of the underlying buffer without copying data, + /// which is efficient for large byte sequences. + /// + /// [length] specifies the number of bytes to read. + /// + /// Example: + /// ```dart + /// final header = reader.readBytes(4); // Read 4-byte header + /// final payload = reader.readBytes(256); // Read payload + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override Uint8List readBytes(int length) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } _checkBounds(length, 'Bytes'); - final bytes = Uint8List.sublistView(_buffer, _offset, _offset + length); - _offset += length; + // Create a view of the underlying buffer without copying + final bOffset = _rs.baseOffset; + final bytes = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length); + + _rs.offset += length; return bytes; } + /// Reads all remaining bytes from the current position to the end of the + /// buffer. + /// + /// Returns a view of the remaining bytes without copying data. + /// Useful for reading trailing data or payloads of unknown length. + /// + /// Example: + /// ```dart + /// final payload = reader.readRemainingBytes(); + /// print('Payload length: ${payload.length}'); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List readRemainingBytes() => readBytes(availableBytes); + + /// Reads a length-prefixed byte array. + /// + /// First reads the length as a VarUint, then reads that many bytes. + /// Returns a view of the underlying buffer without copying data. + /// + /// This is the counterpart to `BinaryWriter.writeVarBytes`. + /// + /// Example: + /// ```dart + /// final data = reader.readVarBytes(); + /// print('Read ${data.length} bytes'); + /// ``` + /// + /// This is equivalent to: + /// ```dart + /// final length = reader.readVarUint(); + /// final data = reader.readBytes(length); + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. + /// + /// Asserts bounds in debug mode if insufficient bytes are available. + @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. + /// + /// [length] is the number of UTF-8 bytes to read (not the number of + /// characters). The string is decoded directly from the buffer without + /// copying. + /// + /// [allowMalformed] controls how invalid UTF-8 sequences are handled: + /// - If true: replaces malformed sequences with U+FFFD (�) + /// - If false (default): throws [FormatException] on invalid UTF-8 + /// + /// **Common pattern:** Read length first, then string: + /// + /// ```dart + /// // Length-prefixed string + /// final byteLength = reader.readVarUint(); + /// final text = reader.readString(byteLength); + /// // Fixed-length magic string + /// final magic = reader.readString(4); // e.g., "PNG\n" + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override String readString(int length, {bool allowMalformed = false}) { if (length == 0) { return ''; @@ -190,43 +447,252 @@ class BinaryReader extends BinaryReaderInterface { _checkBounds(length, 'String'); - final view = Uint8List.sublistView(_buffer, _offset, _offset + length); - _offset += length; + final bOffset = _rs.baseOffset; + final view = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length); + _rs.offset += length; return utf8.decode(view, allowMalformed: allowMalformed); } + /// Reads a length-prefixed UTF-8 encoded string. + /// + /// First reads the UTF-8 byte length as a VarUint, then reads and decodes + /// the UTF-8 string data. + /// + /// [allowMalformed] controls how invalid UTF-8 sequences are handled: + /// - If true: replaces invalid sequences with U+FFFD (�) + /// - If false (default): throws [FormatException] on malformed UTF-8 + /// + /// This is the counterpart to `BinaryWriter.writeVarString`. + /// + /// Example: + /// ```dart + /// final text = reader.readVarString(); + /// print(text); // 'Hello, 世界! 🌍' + /// ``` + /// + /// Throws [RangeError] if attempting to read past buffer end. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + String readVarString({bool allowMalformed = false}) { + final length = readVarUint(); + return readString(length, allowMalformed: allowMalformed); + } + + /// Reads a boolean value (1 byte). + /// + /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. + /// + /// Example: + /// ```dart + /// final isActive = reader.readBool(); // Read active flag + /// ``` + /// Asserts bounds in debug mode if insufficient bytes are available. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + bool readBool() { + final value = readUint8(); + return value != 0; + } + + /// Checks if there are at least [length] bytes available to read. + /// + /// Returns `true` if enough bytes are available, `false` otherwise. + /// + /// Useful for conditional reads when the data format may vary. + /// Example: + /// ```dart + /// if (reader.hasBytes(4)) { + /// final value = reader.readUint32(); + /// // Process value + /// } else { + /// // Handle missing data + /// } + /// ``` + @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 (_rs.offset + length) <= _rs.length; + } + + /// Reads bytes without advancing the read position. + /// + /// This allows inspecting upcoming data without consuming it. + /// Useful for protocol parsing where you need to look ahead. + /// + /// [length] specifies the number of bytes to peek at. + /// [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. + /// + /// Example: + /// ```dart + /// // Check message type without consuming the byte + /// final typeBytes = reader.peekBytes(1); + /// if (typeBytes[0] == 0x42) { + /// // Handle type 0x42 + /// } + /// final actualType = reader.readUint8(); // Now read it + /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override Uint8List peekBytes(int length, [int? offset]) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } if (length == 0) { return Uint8List(0); } - final peekOffset = offset ?? _offset; + final peekOffset = offset ?? _rs.offset; _checkBounds(length, 'Peek Bytes', peekOffset); - return Uint8List.sublistView(_buffer, peekOffset, peekOffset + length); + final bOffset = _rs.baseOffset; + + return _rs.data.buffer.asUint8List(bOffset + peekOffset, length); } - @override + /// Advances the read position by the specified number of bytes. + /// + /// 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. + /// + /// Example: + /// ```dart + /// // Skip optional padding or reserved fields + /// reader.skip(4); // Skip 4 bytes of padding + /// // Skip unknown message payload + /// final payloadSize = reader.readUint32(); + /// reader.skip(payloadSize); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void skip(int length) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } _checkBounds(length, 'Skip'); - _offset += length; + _rs.offset += length; + } + + /// 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. + /// + /// Example: + /// ```dart + /// // Jump to a specific offset to read data + /// reader.seek(128); // Move to byte offset 128 + /// final value = reader.readUint32(); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void seek(int position) { + if (position < 0 || position > _rs.length) { + throw RangeError.range(position, 0, _rs.length, 'position'); + } + _rs.offset = position; + } + + /// 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. + /// + /// Example: + /// ```dart + /// // Re-read the last 4 bytes + /// reader.rewind(4); + /// final value = reader.readUint32(); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void rewind(int length) { + 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; } + /// Resets the read position to the beginning of the buffer. + /// + /// This allows re-reading the same data without creating a new reader. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override void reset() { - _offset = 0; + _rs.offset = 0; } - @override - int get offset => _offset; + /// Internal method to check if enough bytes are available to read. + /// + /// Throws an assertion error in debug mode if not enough bytes. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _checkBounds(int bytes, String type, [int? offset]) { + if (bytes < 0) { + throw RangeError.value(bytes, 'bytes', 'Bytes must be non-negative'); + } + + final start = offset ?? _rs.offset; + final end = start + bytes; + + if (start < 0 || start > _rs.length) { + throw RangeError.range(start, 0, _rs.length, 'offset'); + } + + if (end > _rs.length) { + throw RangeError( + 'Not enough bytes to read $type: required $bytes bytes, available ' + '${_rs.length - _rs.offset} bytes at offset ${_rs.offset}', + ); + } + } +} + +/// Internal state holder for [BinaryReader]. +/// +/// Stores the buffer, read position, and provides efficient typed access +/// through [ByteData]. Separated from the extension type to enable +/// zero-cost abstractions and efficient inline operations. +final class _ReaderState { + _ReaderState(Uint8List buffer) + : list = buffer, + data = ByteData.sublistView(buffer).asUnmodifiableView(), + buffer = buffer.buffer, + length = buffer.length, + baseOffset = buffer.offsetInBytes, + offset = 0; + + /// Direct access to the underlying byte list. + final Uint8List list; + + /// Efficient view for typed data access (getInt32, getFloat64, etc.). + final ByteData data; + + /// The underlying byte buffer. + final ByteBuffer buffer; + + /// Total length of the buffer in bytes. + final int length; + + /// Current read position in the buffer. + late int offset; + + /// Offset of the buffer view within its underlying [ByteBuffer]. + /// Necessary for creating accurate subviews. + final int baseOffset; } diff --git a/lib/src/binary_reader_interface.dart b/lib/src/binary_reader_interface.dart deleted file mode 100644 index fbb77cb..0000000 --- a/lib/src/binary_reader_interface.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'dart:typed_data'; - -/// The [BinaryReaderInterface] class is an abstract base class used to decode -/// various types of data from a binary format. -abstract class BinaryReaderInterface { - /// Returns the number of bytes available to read from the buffer. - /// - /// This getter calculates the difference between the total length of the - /// buffer and the current offset, indicating the remaining bytes that can - /// still be read. - int get availableBytes; - - /// Returns the number of bytes that have been read from the buffer. - /// - /// This getter returns the current offset, indicating how many bytes have - /// been consumed from the buffer since the start. - int get usedBytes; - - /// Reads an 8-bit unsigned integer from the buffer. - /// - /// This method reads an 8-bit unsigned integer from the current offset - /// position and increments the offset by 1 byte. - /// - /// Returns an unsigned 8-bit integer (range: 0 to 255). - /// - /// Example: - /// ```dart - /// int value = reader.readUint8(); // Reads a single byte as an unsigned integer. - /// ``` - int readUint8(); - - /// Reads an 8-bit signed integer from the buffer. - /// - /// This method reads an 8-bit signed integer from the current offset position - /// and increments the offset by 1 byte. - /// - /// Returns a signed 8-bit integer (range: -128 to 127). - /// - /// Example: - /// ```dart - /// int value = reader.readInt8(); // Reads a single byte as a signed integer. - /// ``` - int readInt8(); - - /// Reads a 16-bit unsigned integer from the buffer. - /// - /// This method reads a 16-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 2 bytes. - /// - /// Returns an unsigned 16-bit integer (range: 0 to 65535). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint16(); // Reads two bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint16(Endian.little); // Reads two bytes as an unsigned integer in little-endian order. - /// ``` - int readUint16([Endian endian = Endian.big]); - - /// Reads a 16-bit signed integer from the buffer. - /// - /// This method reads a 16-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 2 - /// bytes. - /// - /// Returns a signed 16-bit integer (range: -32768 to 32767). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt16(); // Reads two bytes as a signed integer in big-endian order. - /// int value = reader.readInt16(Endian.little); // Reads two bytes as a signed integer in little-endian order. - /// ``` - int readInt16([Endian endian = Endian.big]); - - /// Reads a 32-bit unsigned integer from the buffer. - /// - /// This method reads a 32-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 4 bytes. - /// - /// Returns an unsigned 32-bit integer (range: 0 to 4294967295). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint32(); // Reads four bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint32(Endian.little); // Reads four bytes as an unsigned integer in little-endian order. - /// ``` - int readUint32([Endian endian = Endian.big]); - - /// Reads a 32-bit signed integer from the buffer. - /// - /// This method reads a 32-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 4 - /// bytes. - /// - /// Returns a signed 32-bit integer (range: -2147483648 to 2147483647). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt32(); // Reads four bytes as a signed integer in big-endian order. - /// int value = reader.readInt32(Endian.little); // Reads four bytes as a signed integer in little-endian order. - /// ``` - int readInt32([Endian endian = Endian.big]); - - /// Reads a 64-bit unsigned integer from the buffer. - /// - /// This method reads a 64-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 8 bytes. - /// - /// Returns an unsigned 64-bit integer (range: 0 to 18446744073709551615). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint64(); // Reads eight bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint64(Endian.little); // Reads eight bytes as an unsigned integer in little-endian order. - /// ``` - int readUint64([Endian endian = Endian.big]); - - /// Reads a 64-bit signed integer from the buffer. - /// - /// This method reads a 64-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 8 - /// bytes. - /// - /// Returns a signed 64-bit integer - /// (range: -9223372036854775808 to 9223372036854775807). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt64(); // Reads eight bytes as a signed integer in big-endian order. - /// int value = reader.readInt64(Endian.little); // Reads eight bytes as a signed integer in little-endian order. - /// ``` - int readInt64([Endian endian = Endian.big]); - - /// Reads a 32-bit floating point number from the buffer. - /// - /// This method reads a 32-bit float from the current offset position with the - /// specified byte order (endian) and increments the offset by 4 bytes. - /// - /// Returns a 32-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [Endian.big]). - /// - /// Example: - /// ```dart - /// double value = reader.readFloat32(); // Reads four bytes as a float in big-endian order. - /// double value = reader.readFloat32(Endian.little); // Reads four bytes as a float in little-endian order. - /// ``` - double readFloat32([Endian endian = Endian.big]); - - /// Reads a 64-bit floating point number from the buffer. - /// - /// This method reads a 64-bit float from the current offset position with the - /// specified byte order (endian) and increments the offset by 8 bytes. - /// - /// Returns a 64-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// double value = reader.readFloat64(); // Reads eight bytes as a float in big-endian order. - /// double value = reader.readFloat64(Endian.little); // Reads eight bytes as a float in little-endian order. - /// ``` - double readFloat64([Endian endian = Endian.big]); - - /// Reads a list of bytes from the buffer. - /// - /// This method reads the specified number of bytes from the current offset - /// position and increments the offset by that number of bytes. - /// - /// The [length] parameter specifies the number of bytes to read. - /// - /// Returns a [Uint8List] containing the read bytes. - /// - /// Example: - /// ```dart - /// Uint8List bytes = reader.readBytes(5); // Reads five bytes from the buffer. - /// ``` - Uint8List readBytes(int length); - - /// Reads a UTF-8 encoded string from the buffer. - /// - /// This method reads the specified number of bytes from the buffer, decodes - /// them using UTF-8 encoding, and returns the resulting string. The offset - /// is incremented by the length of the read bytes. - /// - /// The [length] parameter specifies the number of bytes to read from the - /// buffer. - /// - /// The optional [allowMalformed] parameter specifies whether to allow - /// malformed UTF-8 sequences (defaults to false). - /// - /// Example: - /// ```dart - /// String value = reader.readString(5); // Reads 5 bytes and decodes them as a UTF-8 string. - /// ``` - String readString(int length, {bool allowMalformed = false}); - - /// Peeks a list of bytes from the buffer without changing the internal state. - /// - /// This method reads the specified number of bytes from the specified offset - /// position and does not change the current offset. - /// - /// The [length] parameter specifies the number of bytes to read. - /// The optional [offset] parameter specifies the offset position to start - /// reading (defaults to the current offset). - /// - /// Returns a [Uint8List] containing the read bytes. - /// - /// Example: - /// ```dart - /// Uint8List bytes = reader.peekBytes(5); // Reads five bytes from the current offset without changing the offset. - /// Uint8List bytes = reader.peekBytes(5, 10); // Reads five bytes from the specified offset (10) without changing the offset. - /// ``` - Uint8List peekBytes(int length, [int? offset]); - - /// Skips the specified number of bytes in the buffer. - /// - /// This method increments the current offset by the specified number of - /// bytes, effectively skipping over that number of bytes in the buffer. - /// - /// The [length] parameter specifies the number of bytes to skip. - /// - /// Example: - /// ```dart - /// reader.skip(5); // Skips the next 5 bytes in the buffer. - /// ``` - void skip(int length); - - /// Resets the reader to the initial state. - /// - /// This method sets the current offset back to 0, allowing the reader to - /// start reading from the beginning of the buffer again. - /// - /// Example: - /// ```dart - /// reader.readUint8(); // Reads a byte - /// reader.reset(); // Resets to the beginning - /// reader.readUint8(); // Reads the same byte again - /// ``` - void reset(); - - /// Returns the current offset position in the buffer. - /// - /// This getter returns the current reading position, which is the same as - /// [usedBytes]. This is useful when you need to save the current position - /// to return to it later. - /// - /// Example: - /// ```dart - /// int position = reader.offset; // Gets current position - /// ``` - int get offset; -} diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 332726a..fd62bcc 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,381 +1,1144 @@ import 'dart:typed_data'; -import 'binary_writer_interface.dart'; +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER +// for explanation of max safe integer in JavaScript. +import 'constants_native.dart' if (dart.library.js_util) 'constants_web.dart'; -/// A high-performance implementation of [BinaryWriterInterface] for encoding -/// data into binary format. +/// A high-performance binary writer for encoding data into a byte buffer. +/// +/// Provides methods for writing various data types including: +/// - Variable-length integers (VarInt, ZigZag) +/// - Fixed-width integers (8, 16, 32, 64-bit signed and unsigned) +/// - Floating-point numbers (32 and 64-bit) +/// - Byte arrays +/// - UTF-8 encoded strings +/// +/// The writer automatically expands its internal buffer as needed. /// /// Example: /// ```dart /// final writer = BinaryWriter(); +/// // Write various data types /// writer.writeUint32(42); -/// writer.writeString('Hello'); -/// final bytes = writer.toBytes(); // View without reset -/// writer.writeUint8(10); // Can continue writing -/// final final = writer.takeBytes(); // View with reset +/// writer.writeFloat64(3.14); +/// // Write length-prefixed string +/// final text = 'Hello, World!'; +/// final utf8Bytes = utf8.encode(text); +/// writer.writeVarUint(utf8Bytes.length); +/// writer.writeString(text); +/// // Extract bytes and optionally reuse writer +/// final bytes = writer.takeBytes(); // Resets writer for reuse +/// // or: final bytes = writer.toBytes(); // Keeps writer state /// ``` -class BinaryWriter extends BinaryWriterInterface { - /// Creates a new [BinaryWriter] with an optional initial buffer size. +extension type BinaryWriter._(_WriterState _ws) { + /// Creates a new [BinaryWriter] with the specified initial buffer size. /// - /// The [initialBufferSize] parameter specifies the initial capacity of the - /// internal buffer (defaults to 64 bytes). Choose a larger value if you - /// expect to write large amounts of data to reduce reallocations. - BinaryWriter({int initialBufferSize = 64}) - : _initialBufferSize = initialBufferSize { - _initializeBuffer(initialBufferSize); - } + /// The buffer will automatically expand as needed when writing data. + /// A larger initial size can improve performance if you know approximately + /// how much data will be written. + /// + /// [initialBufferSize] defaults to 128 bytes. + BinaryWriter({int initialBufferSize = 128}) + : this._(_WriterState(initialBufferSize)); - final int _initialBufferSize; + /// Returns the total number of bytes written to the buffer. + int get bytesWritten => _ws.offset; - /// Internal buffer for storing binary data. - late Uint8List _buffer; + /// Returns the current capacity of the internal buffer. + int get capacity => _ws.capacity; - /// Current write position in the buffer. - var _offset = 0; + /// Writes an unsigned variable-length integer using VarInt encoding. + /// + /// VarInt encoding uses the lower 7 bits of each byte for data and the + /// highest bit as a continuation flag. This is more space-efficient for + /// small unsigned numbers (1-5 bytes for typical 32-bit values). + /// + /// **When to use:** + /// - Counts, lengths, array sizes (always non-negative) + /// - IDs, indices, and other naturally unsigned values + /// - When values are typically small (< 128 uses only 1 byte) + /// + /// **Performance:** Values 0-127 use fast single-byte path. + /// + /// For signed integers that may be negative, use [writeVarInt] instead, + /// which uses ZigZag encoding to efficiently handle negative values. + /// + /// Example: + /// ```dart + /// writer.writeVarUint(42); // 1 byte + /// writer.writeVarUint(300); // 2 bytes + /// writer.writeVarUint(1000000); // 3 bytes + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void writeVarUint(int value) { + // Fast path: single-byte (0-127) + var offset = _ws.offset; + if (value < 0x80 && value >= 0) { + _ws.ensureOneByte(); + _ws.list[offset++] = value; + _ws.offset = offset; + return; + } - /// Cached buffer capacity to avoid repeated length checks. - var _capacity = 0; + _ws.ensureSize(10); + // Slow path: multi-byte VarInt + final list = _ws.list; + + // First byte (always has continuation bit) + list[offset++] = (value & 0x7F) | 0x80; + var v = value >>> 7; + + // Unrolled 2-byte case (covers 0-16383, ~90% of real-world values) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } - @override - int get bytesWritten => _offset; + // Second byte + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + // Unrolled 3-byte case (covers 0-2097151) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } + + // Third byte + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + + // Unrolled 4-byte case (covers 0-268435455, ~99.9% of 32-bit values) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } + + // Generic loop for remaining bytes (rare large 64-bit numbers) + while (v >= 0x80) { + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + } + + list[offset++] = v; // Last byte without continuation bit + _ws.offset = offset; + } + + /// Writes a signed variable-length integer using ZigZag encoding. + /// + /// ZigZag encoding maps signed integers to unsigned integers in a way that + /// small absolute values (both positive and negative) use fewer bytes: + /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. + /// + /// The encoded value is then written using VarInt format. This is more + /// efficient than [writeVarUint] for signed values that may be negative. + /// + /// **When to use:** + /// - Signed values where negatives are common (deltas, offsets) + /// - Values centered around zero + /// - Temperature readings, coordinate deltas, etc. + /// + /// **Performance:** Small absolute values (both + and -) encode efficiently. + /// + /// Example: + /// ```dart + /// writer.writeVarInt(0); // 1 byte + /// writer.writeVarInt(-1); // 1 byte + /// writer.writeVarInt(42); // 1 byte + /// writer.writeVarInt(-42); // 1 byte + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void writeVarInt(int value) { + // ZigZag: (n << 1) ^ (n >> 63) + // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 + final encoded = (value << 1) ^ (value >> 63); + writeVarUint(encoded); + } + + /// Writes an 8-bit unsigned integer (0-255). + /// + /// Example: + /// ```dart + /// writer.writeUint8(0x42); // Write message type + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); - _ensureSize(1); + _ws.ensureOneByte(); - _buffer[_offset++] = value; + _ws.list[_ws.offset++] = value; } + /// Writes an 8-bit signed integer (-128 to 127). + /// + /// Example: + /// ```dart + /// writer.writeInt8(-50); // Write temperature offset + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); - _ensureSize(1); + _ws.ensureOneByte(); - _buffer[_offset++] = value & 0xFF; + _ws.list[_ws.offset++] = value & 0xFF; } + /// Writes a 16-bit unsigned integer (0-65535). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint16(8080); // Port number + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeUint16(int value, [Endian endian = Endian.big]) { + void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); - _ensureSize(2); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - } + _ws.ensureTwoBytes(); + + _ws.data.setUint16(_ws.offset, value, endian); + _ws.offset += 2; } + /// Writes a 16-bit signed integer (-32768 to 32767). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt16(-100); // Temperature in Celsius + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeInt16(int value, [Endian endian = Endian.big]) { + void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); - _ensureSize(2); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - } + _ws.ensureTwoBytes(); + + _ws.data.setInt16(_ws.offset, value, endian); + _ws.offset += 2; } + /// Writes a 32-bit unsigned integer (0 to 4,294,967,295). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint32(1640995200); // Unix timestamp + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeUint32(int value, [Endian endian = Endian.big]) { + void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); - _ensureSize(4); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - } + _ws.ensureFourBytes(); + + _ws.data.setUint32(_ws.offset, value, endian); + _ws.offset += 4; } + /// Writes a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt32(-500000); // Account balance in cents + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeInt32(int value, [Endian endian = Endian.big]) { + void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); - _ensureSize(4); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - } + _ws.ensureFourBytes(); + + _ws.data.setInt32(_ws.offset, value, endian); + _ws.offset += 4; } + /// Writes a 64-bit unsigned integer. + /// + /// **Note:** Since Dart's `int` type is a signed 64-bit integer, this method + /// is limited to the range 0 to 2^63 - 1 (9,223,372,036,854,775,807). + /// Values above this cannot be represented as positive integers in Dart. + /// + /// On web targets, precision is further limited to 2^53. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint64(9007199254740991); // Max safe JS int + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeUint64(int value, [Endian endian = Endian.big]) { - _checkRange(value, 0, 9223372036854775807, 'Uint64'); - _ensureSize(8); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 56) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 56) & 0xFF; - } + void writeUint64(int value, [Endian endian = .big]) { + _checkRange(value, 0, kMaxInt64, 'Uint64'); + _ws.ensureEightBytes(); + + _ws.data.setUint64(_ws.offset, value, endian); + _ws.offset += 8; } + /// Writes a 64-bit signed integer. + /// + /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt64(1234567890123456); // Large ID + /// ``` + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeInt64(int value, [Endian endian = Endian.big]) { - _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); - _ensureSize(8); - - if (endian == Endian.big) { - _buffer[_offset++] = (value >> 56) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 56) & 0xFF; - } - } + void writeInt64(int value, [Endian endian = .big]) { + _checkRange(value, kMinInt64, kMaxInt64, 'Int64'); + _ws.ensureEightBytes(); - // Instance-level temporary buffers for float conversion (thread-safe) - final _tempU8 = Uint8List(8); - late final _tempF32 = Float32List.view(_tempU8.buffer); - late final _tempF64 = Float64List.view(_tempU8.buffer); + _ws.data.setInt64(_ws.offset, value, endian); + _ws.offset += 8; + } + /// Writes a 32-bit floating-point number (IEEE 754 single precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeFloat32(3.14); // Pi approximation + /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeFloat32(double value, [Endian endian = Endian.big]) { - _ensureSize(4); - _tempF32[0] = value; // Write to temp buffer - if (endian == Endian.big) { - _buffer[_offset++] = _tempU8[3]; - _buffer[_offset++] = _tempU8[2]; - _buffer[_offset++] = _tempU8[1]; - _buffer[_offset++] = _tempU8[0]; - } else { - _buffer.setRange(_offset, _offset + 4, _tempU8); - _offset += 4; - } + void writeFloat32(double value, [Endian endian = .big]) { + _ws.ensureFourBytes(); + _ws.data.setFloat32(_ws.offset, value, endian); + _ws.offset += 4; } + /// Writes a 64-bit floating-point number (IEEE 754 double precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeFloat64(3.14159265359); // High-precision pi + /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeFloat64(double value, [Endian endian = Endian.big]) { - _ensureSize(8); - _tempF64[0] = value; - if (endian == Endian.big) { - _buffer[_offset++] = _tempU8[7]; - _buffer[_offset++] = _tempU8[6]; - _buffer[_offset++] = _tempU8[5]; - _buffer[_offset++] = _tempU8[4]; - _buffer[_offset++] = _tempU8[3]; - _buffer[_offset++] = _tempU8[2]; - _buffer[_offset++] = _tempU8[1]; - _buffer[_offset++] = _tempU8[0]; - } else { - _buffer.setRange(_offset, _offset + 8, _tempU8); - _offset += 8; - } + void writeFloat64(double value, [Endian endian = .big]) { + _ws.ensureEightBytes(); + _ws.data.setFloat64(_ws.offset, value, endian); + _ws.offset += 8; } + /// Writes a sequence of bytes from the given list. + /// + /// [offset] specifies the starting position in [bytes] (defaults to 0). + /// [length] specifies how many bytes to write (defaults to remaining bytes). + /// + /// Example: + /// ```dart + /// final data = [1, 2, 3, 4, 5]; + /// writer.writeBytes(data); // Write all 5 bytes + /// writer.writeBytes(data, 2); // Write [3, 4, 5] + /// writer.writeBytes(data, 1, 3); // Write [2, 3, 4] + /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override - void writeBytes(Iterable bytes) { - // Early return for empty byte lists - if (bytes.isEmpty) { - return; + void writeBytes(List bytes, [int offset = 0, int? length]) { + 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'); + } + + final len = length ?? (bytes.length - offset); + + if (len < 0) { + throw RangeError.value(len, 'length', 'Length must be non-negative'); + } + + if (offset + len > bytes.length) { + throw RangeError( + 'Offset + length exceeds list length: ' + 'offset=$offset length=$len ' + 'listLength=${bytes.length}', + ); } - final length = bytes.length; - _ensureSize(length); + _ws.ensureSize(len); - _buffer.setRange(_offset, _offset + length, bytes); - _offset += length; + _ws.list.setRange(_ws.offset, _ws.offset + len, bytes, offset); + _ws.offset += len; } + /// Writes a length-prefixed byte array. + /// + /// First writes the length as a VarUint, followed by the byte data. + /// This is useful for serializing binary blobs of unknown size. + /// + /// This is the counterpart to `BinaryReader.readVarBytes`. + /// + /// Example: + /// ```dart + /// final imageData = [/* ... binary data ... */]; + /// writer.writeVarBytes(imageData); + /// // Length is automatically written as VarUint + /// ``` + /// + /// This is equivalent to: + /// ```dart + /// writer.writeVarUint(bytes.length); + /// writer.writeBytes(bytes); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void writeVarBytes(List bytes) { + writeVarUint(bytes.length); + writeBytes(bytes); + } + + /// Writes a UTF-8 encoded string. + /// + /// The string is encoded directly to UTF-8 bytes with optimized handling for: + /// - ASCII fast path (unrolled loops for better performance) + /// - Multi-byte UTF-8 sequences (Cyrillic, CJK, emojis, etc.) + /// - Proper surrogate pair handling for characters outside the BMP + /// + /// [allowMalformed] controls how invalid UTF-16 sequences are handled: + /// - If true (default): replaces lone surrogates with U+FFFD (�) + /// - If false: throws [FormatException] on malformed input + /// + /// **Important:** This does NOT write the string length. For self-describing + /// data, write the length first: + /// + /// Example: + /// ```dart + /// // Length-prefixed string (recommended for most protocols) + /// final text = 'Hello, 世界! 🌍'; + /// final utf8Bytes = utf8.encode(text); + /// writer.writeVarUint(utf8Bytes.length); // Write byte length + /// writer.writeString(text); // Write string data + /// // Or for simple fixed-length strings: + /// writer.writeString('MAGIC'); // No length prefix needed + /// ``` + /// + /// **Performance:** Highly optimized for ASCII-heavy strings. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - @override void writeString(String value, {bool allowMalformed = true}) { final len = value.length; if (len == 0) { return; } - // Over-allocate max UTF-8 size (4 bytes/char) - _ensureSize(len * 4); + // Pre-allocate buffer: worst case is 3 bytes per UTF-16 code unit + // Most common case: 1 byte/char (ASCII) or 2-3 bytes/char (non-ASCII) + // Surrogate pairs: 2 units -> 4 bytes UTF-8 (2 bytes per unit average) + _ws.ensureSize(len * 3); + + final list = _ws.list; + var offset = _ws.offset; + var i = 0; - var bufIdx = _offset; - for (var i = 0; i < len; i++) { + while (i < len) { var c = value.codeUnitAt(i); - if (c < 128) { - _buffer[bufIdx++] = c; - } else if (c < 2048) { - _buffer[bufIdx++] = 192 | (c >> 6); - _buffer[bufIdx++] = 128 | (c & 63); - } else if (c >= 0xD800 && c <= 0xDBFF) { - // High surrogate - if (i + 1 < len) { - final next = value.codeUnitAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { - // Valid surrogate pair - i++; - c = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); - _buffer[bufIdx++] = 240 | (c >> 18); - _buffer[bufIdx++] = 128 | ((c >> 12) & 63); - _buffer[bufIdx++] = 128 | ((c >> 6) & 63); - _buffer[bufIdx++] = 128 | (c & 63); - continue; + + if (c < 0x80) { + // ------------------------------------------------------- + // ASCII Fast Path: Optimized for common case + // Most strings contain primarily ASCII, so we optimize this path + // with unrolled loops to process 4 characters at a time. + // ------------------------------------------------------- + list[offset++] = c; + i++; + + // Unrolled loop: process 4 ASCII chars at once + // Bitwise OR (|) checks if any char >= 0x80 in one operation + while (i <= len - 4) { + final c0 = value.codeUnitAt(i); + final c1 = value.codeUnitAt(i + 1); + final c2 = value.codeUnitAt(i + 2); + final c3 = value.codeUnitAt(i + 3); + + if ((c0 | c1 | c2 | c3) < 0x80) { + list[offset] = c0; + list[offset + 1] = c1; + list[offset + 2] = c2; + list[offset + 3] = c3; + offset += 4; + i += 4; + } else { + break; } } - // Lone high surrogate - if (!allowMalformed) { - throw FormatException( - 'Invalid UTF-16: lone high surrogate at index $i', - value, - i, - ); + + // Catch remaining ASCII characters before multi-byte logic + while (i < len) { + c = value.codeUnitAt(i); + if (c >= 0x80) { + break; + } + list[offset++] = c; + i++; + } + + if (i == len) { + break; } - // Replacement char U+FFFD - _buffer[bufIdx++] = 0xEF; - _buffer[bufIdx++] = 0xBF; - _buffer[bufIdx++] = 0xBD; - } else if (c >= 0xDC00 && c <= 0xDFFF) { - // Lone low surrogate - if (!allowMalformed) { - throw FormatException( - 'Invalid UTF-16: lone low surrogate at index $i', - value, - i, - ); + } + + // ------------------------------------------------------- + // Multi-byte UTF-8 encoding + // UTF-8 uses 2-4 bytes for non-ASCII characters + // ------------------------------------------------------- + if (c < 0x800) { + // 2-byte sequence: U+0080 to U+07FF + // Covers: Latin Extended, Greek, Cyrillic, Arabic, Hebrew, etc. + list[offset++] = 0xC0 | (c >> 6); + list[offset++] = 0x80 | (c & 0x3F); + i++; + } else if (c < 0xD800 || c > 0xDFFF) { + // 3-byte sequence: U+0800 to U+FFFF (excluding surrogates) + // Covers: CJK characters, most world scripts, symbols, etc. + list[offset++] = 0xE0 | (c >> 12); + list[offset++] = 0x80 | ((c >> 6) & 0x3F); + list[offset++] = 0x80 | (c & 0x3F); + i++; + } else if (c <= 0xDBFF && i + 1 < len) { + // 4-byte sequence: U+10000 to U+10FFFF via surrogate pairs + // High surrogate (0xD800-0xDBFF) must be followed by low + // (0xDC00-0xDFFF) + // Covers: Emojis, historic scripts, rare CJK, musical notation, etc. + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + // Valid surrogate pair: combine high and low surrogates + // Formula: 0x10000 + ((high & 0x3FF) << 10) + (low & 0x3FF) + final codePoint = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); + list[offset++] = 0xF0 | (codePoint >> 18); + list[offset++] = 0x80 | ((codePoint >> 12) & 0x3F); + list[offset++] = 0x80 | ((codePoint >> 6) & 0x3F); + list[offset++] = 0x80 | (codePoint & 0x3F); + i += 2; + } else { + // Invalid: high surrogate not followed by low surrogate + offset = _handleMalformed(value, i, offset, allowMalformed); + i++; } - // Replacement char U+FFFD - _buffer[bufIdx++] = 0xEF; - _buffer[bufIdx++] = 0xBF; - _buffer[bufIdx++] = 0xBD; } else { - // 3 bytes - _buffer[bufIdx++] = 224 | (c >> 12); - _buffer[bufIdx++] = 128 | ((c >> 6) & 63); - _buffer[bufIdx++] = 128 | (c & 63); + // Malformed UTF-16: lone low surrogate or high surrogate at end + offset = _handleMalformed(value, i, offset, allowMalformed); + i++; } } - _offset = bufIdx; + _ws.offset = offset; } - @override - Uint8List takeBytes() { - final result = Uint8List.sublistView(_buffer, 0, _offset); - - _offset = 0; - _initializeBuffer(_initialBufferSize); + /// Writes a length-prefixed UTF-8 encoded string. + /// + /// First writes the UTF-8 byte length as a VarUint, followed by the + /// UTF-8 encoded string data. + /// + /// [allowMalformed] controls how invalid UTF-16 sequences are handled: + /// - If true (default): replaces lone surrogates with U+FFFD (�) + /// - If false: throws [FormatException] on malformed input + /// + /// Example: + /// ```dart + /// final text = 'Hello, 世界! 🌍'; + /// writer.writeVarString(text); + /// ``` + /// This is equivalent to: + /// ```dart + /// final utf8Bytes = utf8.encode(text); + /// writer.writeVarUint(utf8Bytes.length); + /// writer.writeString(text); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void writeVarString(String value, {bool allowMalformed = true}) { + final utf8Length = getUtf8Length(value); + writeVarUint(utf8Length); + writeString(value, allowMalformed: allowMalformed); + } - return result; + /// Writes a boolean value as a single byte. + /// + /// `true` is written as `1` and `false` as `0`. + /// + /// Example: + /// ```dart + /// writer.writeBool(true); // Writes byte 0x01 + /// writer.writeBool(false); // Writes byte 0x00 + /// ``` + /// + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + // Disable lint to allow positional boolean parameter for simplicity + // ignore: avoid_positional_boolean_parameters + void writeBool(bool value) { + writeUint8(value ? 1 : 0); } - @override - Uint8List toBytes() => Uint8List.sublistView(_buffer, 0, _offset); + /// Extracts all written bytes and resets the writer. + /// + /// After calling this method, the writer is reset and ready for reuse. + /// This is more efficient than creating a new writer for each operation. + /// + /// Returns a view of the written bytes (no copying occurs). + /// + /// **Use case:** When you're done with this batch and want to start fresh. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// final packet1 = writer.takeBytes(); // Get bytes and reset + /// writer.writeUint32(100); // Writer is ready for reuse + /// final packet2 = writer.takeBytes(); + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + Uint8List takeBytes() { + final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); + _ws._initializeBuffer(); - @override - void reset() { - _offset = 0; - _initializeBuffer(_initialBufferSize); + return result; } - /// Initializes the buffer with the specified size. + /// Returns a view of the written bytes without resetting the writer. + /// + /// Unlike [takeBytes], this does not reset the writer's state. + /// Subsequent writes will continue appending to the buffer. + /// + /// **Use case:** When you need to inspect or copy data mid-stream. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// final snapshot = writer.toBytes(); // Peek at current data + /// writer.writeUint32(100); // Continue writing + /// final final = writer.takeBytes(); // Get all data + /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _initializeBuffer(int size) { - _buffer = Uint8List(size); - _capacity = size; - } + Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); - /// Checks if the [value] is within the specified [min] and [max] range. + /// Resets the writer to its initial state, discarding all written data. + @pragma('vm:prefer-inline') + void reset() => _ws._initializeBuffer(); + + /// Handles malformed UTF-16 sequences (lone surrogates). /// - /// Throws a [RangeError] if the value is out of bounds. + /// If [allow] is false, throws [FormatException]. + /// Otherwise, writes the Unicode replacement character U+FFFD (�) + /// encoded as UTF-8: 0xEF 0xBF 0xBD @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') + int _handleMalformed(String v, int i, int offset, bool allow) { + if (!allow) { + throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); + } + // Write UTF-8 encoding of U+FFFD replacement character (�) + final list = _ws.list; + list[offset] = 0xEF; + list[offset + 1] = 0xBF; + list[offset + 2] = 0xBD; + return offset + 3; + } + + @pragma('vm:prefer-inline') void _checkRange(int value, int min, int max, String typeName) { if (value < min || value > max) { throw RangeError.range(value, min, max, typeName); } } +} + +/// Internal state holder for [BinaryWriter]. +/// +/// Manages the underlying buffer, capacity tracking, and expansion logic. +/// Separated from the extension type to allow efficient inline operations. +final class _WriterState { + _WriterState(int initialBufferSize) + : this._validated(_validateInitialBufferSize(initialBufferSize)); + + _WriterState._validated(int size) + : _size = size, + capacity = (size + 63) & ~63, + offset = 0, + list = Uint8List((size + 63) & ~63) { + data = list.buffer.asByteData(); + } + + static int _validateInitialBufferSize(int value) { + if (value <= 0) { + throw RangeError.value( + value, + 'initialBufferSize', + 'Initial buffer size must be positive', + ); + } + return value; + } + + /// Current write position in the buffer. + late int offset; + + /// Cached buffer capacity to avoid repeated length checks. + late int capacity; + + /// Underlying byte buffer. + late Uint8List list; + + /// ByteData view of the underlying buffer for efficient writes. + late ByteData data; + + /// Initial buffer size. + final int _size; + + var _isInPool = false; - /// Ensures that the buffer has enough space to accommodate the specified - /// [size] bytes. - /// - /// If the buffer is too small, it expands using a 1.5x growth strategy, - /// which balances memory usage and reallocation frequency. - /// Uses O(1) calculation instead of loop for better performance. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _ensureSize(int size) { - final req = _offset + size; - if (req <= _capacity) { + void _initializeBuffer() { + final alignedSize = (_size + 63) & ~63; + list = Uint8List(alignedSize); + data = list.buffer.asByteData(); + capacity = alignedSize; + offset = 0; + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void ensureSize(int size) { + if (offset + size <= capacity) { + return; + } + + _expand(size); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void ensureOneByte() { + if (offset + 1 <= capacity) { return; } - var newCapacity = _capacity * 3 ~/ 2; // 1.5x + _expand(1); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void ensureTwoBytes() { + if (offset + 2 <= capacity) { + return; + } + + _expand(2); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void ensureFourBytes() { + if (offset + 4 <= capacity) { + return; + } + + _expand(4); + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void ensureEightBytes() { + if (offset + 8 <= capacity) { + return; + } + + _expand(8); + } + + /// Expands the buffer to accommodate additional data. + /// + /// Uses exponential growth (1.5x) for better memory efficiency, + /// but ensures the buffer is always large enough for the requested size. + void _expand(int size) { + final req = offset + size; + // Grow by 1.5x (exponential growth with better memory efficiency) + var newCapacity = capacity + (capacity >> 1); + + // Ensure we meet the minimum requirement if (newCapacity < req) { newCapacity = req; } + // Align to 64-byte boundary + newCapacity = (newCapacity + 63) & ~63; + + list = Uint8List(newCapacity)..setRange(0, offset, list); + + data = list.buffer.asByteData(); + capacity = newCapacity; + } +} + +/// Calculates the UTF-8 byte length of the given string without encoding it. +/// +/// This function efficiently computes the number of bytes required to +/// encode the string in UTF-8, taking into account multi-byte characters +/// and surrogate pairs. It's optimized with an ASCII fast path that processes +/// up to 4 ASCII characters at once. +/// +/// Useful for: +/// - Pre-allocating buffers of the correct size +/// - Calculating message sizes before serialization +/// - Validating string length constraints +/// +/// Performance: +/// - ASCII strings: ~4 bytes per loop iteration +/// - Mixed content: Falls back to character-by-character analysis +/// +/// Example: +/// ```dart +/// final text = 'Hello, 世界! 🌍'; +/// final byteLength = getUtf8Length(text); // 20 bytes +/// // vs text.length would be 15 characters +/// ``` +/// +/// @param s The input string. +/// @return The number of bytes needed for UTF-8 encoding. +int getUtf8Length(String value) { + if (value.isEmpty) { + return 0; + } + + final len = value.length; + var bytes = 0; + var i = 0; + + while (i < len) { + final char = value.codeUnitAt(i); + + // ASCII fast path + if (char < 0x80) { + // Process 4 ASCII characters at a time + final end = len - 4; + while (i <= end) { + final mask = + value.codeUnitAt(i) | + value.codeUnitAt(i + 1) | + value.codeUnitAt(i + 2) | + value.codeUnitAt(i + 3); + + if (mask >= 0x80) { + break; + } + + i += 4; + bytes += 4; + } + + // Handle remaining ASCII characters + while (i < len && value.codeUnitAt(i) < 0x80) { + i++; + bytes++; + } + if (i >= len) { + return bytes; + } + continue; + } - final newBuffer = Uint8List(newCapacity)..setRange(0, _offset, _buffer); - _buffer = newBuffer; - _capacity = newCapacity; + // 2-byte sequence + if (char < 0x800) { + bytes += 2; + i++; + } + // 3-byte sequence + else if (char >= 0xD800 && char <= 0xDBFF && i + 1 < len) { + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + bytes += 4; + i += 2; + continue; + } + // Malformed surrogate pair + bytes += 3; + i++; + } + // 3-byte sequence + else { + bytes += 3; + i++; + } } + + return bytes; +} + +// Disable lint to allow static-only class for pooling +// ignore: avoid_classes_with_only_static_members +/// Object pool for reusing [BinaryWriter] instances to reduce GC pressure. +/// +/// This pool maintains a cache of [BinaryWriter] instances with their +/// internal buffers, allowing efficient reuse without allocating new memory +/// for each write operation. +/// +/// ## Features +/// - **Automatic reuse:** [acquire] gets a pooled writer or creates a new one +/// - **Memory bounds:** Only reuses writers with buffers ≤ 64 KiB +/// - **Size limits:** Maintains max 32 pooled instances +/// - **Safe:** Prevents double-release and handles edge cases +/// +/// ## Usage Pattern +/// Use `acquire()` and `release()` for short-lived write operations: +/// +/// ```dart +/// final writer = BinaryWriterPool.acquire(); +/// try { +/// writer.writeUint32(42); +/// writer.writeString('Hello'); +/// final bytes = writer.toBytes(); +/// // Use bytes... +/// } finally { +/// BinaryWriterPool.release(writer); // Return to pool +/// } +/// ``` +/// +/// ## Thread Safety +/// This pool is isolate-local. Each Dart isolate maintains its own +/// static pool instance. +/// +/// Avoid sharing [BinaryWriter] instances between different isolates. +/// For concurrent operations within the same isolate, ensure writers +/// are acquired and released synchronously or protected by logic +/// to prevent interleaved usage. +/// +/// ## Performance Considerations +/// - Pooling is beneficial for high-frequency write operations +/// - Overhead is minimal for single-use writers (use regular constructor) +/// - Large buffers (>64 KiB) are discarded to avoid memory waste +/// +/// ## Memory Management +/// - Pool max size: 32 writers +/// - Max reusable buffer: 64 KiB +/// - Default buffer size: 1 KiB +/// - Use [clear] to free pooled memory explicitly +/// +/// See also: [BinaryWriter], [stats] for pool monitoring +abstract final class BinaryWriterPool { + // The internal pool of reusable writer states. + static final _pool = <_WriterState>[]; + + /// Maximum number of writers to keep in the pool. + static const _maxPoolSize = 32; + + /// Default initial buffer size for new writers (1 KiB). + static const _defaultBufferSize = 1024; + + /// Maximum buffer capacity allowed for pooling (64 KiB). + /// Writers that exceed this size are discarded to free up system memory + static const int _maxReusableCapacity = 64 * 1024; + + // Performance counters + static var _acquireHit = 0; + static var _acquireMiss = 0; + static var _peakPoolSize = 0; + static var _discardedLargeBuffers = 0; + + /// Acquires a [BinaryWriter] from the pool or creates a new one. + /// + /// Returns a pooled writer if available, otherwise creates a fresh instance + /// with the default buffer size (1 KiB). + /// + /// The returned writer is ready to use and should be returned to the pool + /// via [release] when no longer needed. + /// + /// **Best Practice:** Always use a `try-finally` block. + /// + /// There are two ways to get the data: + /// 1. Use [BinaryWriter.toBytes] if you consume data **inside** the try + /// block (zero-copy view). + /// 2. Use [BinaryWriter.takeBytes] if you need to **return** the data + /// (transfers buffer ownership). + /// + /// ```dart + /// final writer = BinaryWriterPool.acquire(); + /// try { + /// writer.writeUint32(123); + /// return writer.toBytes(); + /// } finally { + /// BinaryWriterPool.release(writer); + /// } + /// ``` + /// + /// Returns: A [BinaryWriter] ready for use. + static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { + if (_pool.isNotEmpty) { + _acquireHit++; + final state = _pool.removeLast().._isInPool = false; + return BinaryWriter._(state); + } + + _acquireMiss++; + return BinaryWriter(initialBufferSize: defaultBufferSize); + } + + /// Returns a [BinaryWriter] to the pool for future reuse. + /// + /// The writer is reset (offset cleared) and stored for future [acquire] + /// calls. Writers with buffers larger than 64 KiB are not pooled to avoid + /// long-term memory retention. + /// + /// **Safe to call multiple times** (duplicate releases are ignored). + /// + /// Only writers with capacity ≤ 64 KiB are pooled. Writers exceeding this + /// limit are discarded, allowing the buffer to be garbage collected. + /// + /// **Do NOT use the writer after releasing it:** + /// + /// ```dart + /// final writer = BinaryWriterPool.acquire(); + /// writer.writeUint32(42); + /// final bytes = writer.toBytes(); + /// BinaryWriterPool.release(writer); + /// // DON'T USE: writer.writeString('invalid'); + /// ``` + /// + /// Parameters: + /// - [writer]: The [BinaryWriter] to return to the pool + static void release(BinaryWriter writer) { + final state = writer._ws; + + // Prevent double-release and state corruption + if (state._isInPool) { + return; + } + + // Only pool writers with reasonable buffer sizes + // Prevents memory bloat from occasional large allocations + if (state.capacity <= _maxReusableCapacity && _pool.length < _maxPoolSize) { + state + ..offset = 0 + .._isInPool = true; + _pool.add(state); + + // Track peak pool size + if (_pool.length > _peakPoolSize) { + _peakPoolSize = _pool.length; + } + } else if (state.capacity > _maxReusableCapacity) { + _discardedLargeBuffers++; + } + } + + /// Returns pool statistics for monitoring and debugging. + /// + /// Useful for performance analysis and detecting pool inefficiencies. + /// + /// Returns a map with keys: + /// - `'pooled'`: Number of writers currently in the pool + /// - `'maxPoolSize'`: Maximum pool capacity + /// - `'defaultBufferSize'`: Initial buffer size for new writers + /// - `'maxReusableCapacity'`: Maximum buffer size for pooling + /// - `'acquireHit'`: Number of successful reuses from pool + /// - `'acquireMiss'`: Number of new writer allocations + /// - `'peakPoolSize'`: Maximum pool size reached + /// - `'discardedLargeBuffers'`: Number of oversized buffers discarded + /// + /// Example: + /// ```dart + /// final stats = BinaryWriterPool.stats; + /// print('Pooled writers: ${stats.pooled}'); // 5 + /// print('Hit rate: ${stats.acquireHit / (stats.acquireHit + stats.acquireMiss)}'); + /// ``` + static PoolStatistics get stats => PoolStatistics({ + 'pooled': _pool.length, + 'maxPoolSize': _maxPoolSize, + 'defaultBufferSize': _defaultBufferSize, + 'maxReusableCapacity': _maxReusableCapacity, + 'acquireHit': _acquireHit, + 'acquireMiss': _acquireMiss, + 'peakPoolSize': _peakPoolSize, + 'discardedLargeBuffers': _discardedLargeBuffers, + }); + + /// Clears the pool, releasing all cached writers. + /// + /// Use this to: + /// - Free memory during low-activity periods + /// - Reset pool state in tests + /// - Handle memory pressure + /// + /// After clearing, subsequent [acquire] calls will create new writers. + /// + /// Example: + /// ```dart + /// BinaryWriterPool.clear(); // All pooled writers discarded + /// ``` + static void clear() { + _pool.clear(); + _acquireHit = 0; + _acquireMiss = 0; + _peakPoolSize = 0; + _discardedLargeBuffers = 0; + } +} + +extension type PoolStatistics(Map _stats) { + /// Number of writers currently in the pool. + int get pooled => _stats['pooled']!; + + /// Maximum pool capacity. + int get maxPoolSize => _stats['maxPoolSize']!; + + /// Initial buffer size for new writers. + int get defaultBufferSize => _stats['defaultBufferSize']!; + + /// Maximum buffer size for pooling. + int get maxReusableCapacity => _stats['maxReusableCapacity']!; + + /// Number of successful reuses from pool (cache hits). + int get acquireHit => _stats['acquireHit']!; + + /// Number of new writer allocations (cache misses). + int get acquireMiss => _stats['acquireMiss']!; + + /// Maximum pool size reached during runtime. + int get peakPoolSize => _stats['peakPoolSize']!; + + /// Number of oversized buffers discarded to prevent memory bloat. + int get discardedLargeBuffers => _stats['discardedLargeBuffers']!; + + /// Total number of acquire operations. + int get totalAcquires => acquireHit + acquireMiss; + + /// Cache hit rate (0.0 to 1.0). + double get hitRate => totalAcquires > 0 ? acquireHit / totalAcquires : 0.0; } diff --git a/lib/src/binary_writer_interface.dart b/lib/src/binary_writer_interface.dart deleted file mode 100644 index f0a8c1c..0000000 --- a/lib/src/binary_writer_interface.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'dart:typed_data'; - -/// The [BinaryWriterInterface] class is an abstract base class used to encode -/// various types of data into a binary format. -abstract class BinaryWriterInterface { - /// Returns the number of bytes written to the buffer. - int get bytesWritten; - - /// Writes an 8-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 8-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position, and the offset is - /// incremented by 1 byte. - /// - /// The [value] parameter must be an unsigned 8-bit integer - /// (range: 0 to 255). - /// - /// Example: - /// ```dart - /// writer.writeUint8(200); // Writes the value 200 as a single byte. - /// ``` - void writeUint8(int value); - - /// Writes an 8-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 8-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position, and the offset is - /// incremented by 1 byte. - /// - /// The [value] parameter must be a signed 8-bit integer - /// (range: -128 to 127). - /// - /// Example: - /// ```dart - /// writer.writeInt8(-5); // Writes the value -5 as a single byte. - /// ``` - void writeInt8(int value); - - /// Writes a 16-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 16-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 2 bytes. - /// - /// The [value] parameter must be an unsigned 16-bit integer - /// (range: 0 to 65535). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint16(500); // Writes the value 500 as two bytes in big-endian order. - /// writer.writeUint16(500, Endian.little); // Writes the value 500 as two bytes in little-endian order. - /// ``` - void writeUint16(int value, [Endian endian = Endian.big]); - - /// Writes a 16-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 16-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 2 bytes. - /// - /// The [value] parameter must be a signed 16-bit integer - /// (range: -32768 to 32767). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeInt16(-100); // Writes the value -100 as two bytes in big-endian order. - /// writer.writeInt16(-100, Endian.little); // Writes the value -100 as two bytes in little-endian order. - /// ``` - void writeInt16(int value, [Endian endian = Endian.big]); - - /// Writes a 32-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be an unsigned 32-bit integer - /// (range: 0 to 4294967295). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint32(100000); // Writes the value 100000 as four bytes in big-endian order. - /// writer.writeUint32(100000, Endian.little); // Writes the value 100000 as four bytes in little-endian order. - /// ``` - void writeUint32(int value, [Endian endian = Endian.big]); - - /// Writes a 32-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be a signed 32-bit integer - /// (range: -2147483648 to 2147483647). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// writer.writeInt32(-50000); // Writes the value -50000 as four bytes in big-endian order. - /// writer.writeInt32(-50000, Endian.little); // Writes the value -50000 as four bytes in little-endian order. - /// ``` - void writeInt32(int value, [Endian endian = Endian.big]); - - /// Writes a 64-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the - /// specified byte order (endian), and the offset is incremented by 8 - /// bytes. - /// - /// The [value] parameter must be an unsigned 64-bit integer - /// (range: 0 to 18446744073709551615). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint64(10000000000); // Writes the value 10000000000 as eight bytes in big-endian order. - /// writer.writeUint64(10000000000, Endian.little); // Writes the value 10000000000 as eight bytes in little-endian order. - /// ``` - void writeUint64(int value, [Endian endian = Endian.big]); - - /// Writes a 64-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the - /// specified byte order (endian), and the offset is incremented by 8 - /// bytes. - /// - /// The [value] parameter must be a signed 64-bit integer - /// (range: -9223372036854775808 to 9223372036854775807). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeInt64(-10000000000); // Writes the value -10000000000 as eight bytes in big-endian order. - /// writer.writeInt64(-10000000000, Endian.little); // Writes the value -10000000000 as eight bytes in little-endian order. - /// ``` - void writeInt64(int value, [Endian endian = Endian.big]); - - /// Writes a 32-bit floating point number to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit float. If necessary, it expands the buffer size. The float is then - /// written at the current offset position with the specified byte - /// order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be a 32-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [Endian.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeFloat32(3.14); // Writes the value 3.14 as four bytes in big-endian order. - /// writer.writeFloat32(3.14, Endian.little); // Writes the value 3.14 as four bytes in little-endian order. - /// ``` - void writeFloat32(double value, [Endian endian = Endian.big]); - - /// Writes a 64-bit floating point number to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit float. If necessary, it expands the buffer size. The float is then - /// written at the current offset position with the specified byte - /// order (endian), and the offset is incremented by 8 bytes. - /// - /// The [value] parameter must be a 64-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). - /// - /// Example: - /// ```dart - /// writer.writeFloat64(3.14); // Writes the value 3.14 as eight bytes in big-endian order. - /// writer.writeFloat64(3.14, Endian.little); // Writes the value 3.14 as eight bytes in little-endian order. - /// ``` - void writeFloat64(double value, [Endian endian = Endian.big]); - - /// Writes a list of bytes to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// provided list of bytes. If necessary, it expands the buffer size. The - /// bytes are then written at the current offset position. If the offset is 0, - /// the bytes are added directly to the builder. Otherwise, the bytes are - /// copied to the buffer, either directly (if the list is a [Uint8List]) or - /// byte by byte. - /// - /// The [bytes] parameter must be a list of integers, where each integer is - /// between 0 and 255 inclusive. The list may be retained until [takeBytes] - /// is called. - /// - /// Example: - /// ```dart - /// writer.writeBytes([1, 2, 3, 4, 5]); // Writes the bytes 1, 2, 3, 4, and 5 to the buffer. - /// ``` - void writeBytes(Iterable bytes); - - /// Writes a UTF-8 encoded string to the buffer. - /// - /// This method encodes the provided string using UTF-8 encoding and writes - /// the resulting bytes to the buffer. If necessary, it expands the buffer - /// size to accommodate the encoded string. The encoded bytes are then written - /// at the current offset position, and the offset is incremented by the - /// length of the encoded string. - /// - /// The [value] parameter is the string to be encoded and written to the - /// buffer. - /// - /// The optional [allowMalformed] parameter specifies whether to allow - /// malformed UTF-16 sequences (lone surrogates). If false, a - /// [FormatException] - /// is thrown when encountering invalid surrogate pairs. If true (default), - /// invalid surrogates are replaced with the Unicode replacement character - /// U+FFFD (�). - /// - /// Example: - /// ```dart - /// writer.writeString("Hello, world!"); // Writes the string "Hello, world!" as UTF-8 bytes to the buffer. - /// writer.writeString("Test\uD800End", allowMalformed: false); // Throws FormatException for lone surrogate - /// ``` - void writeString(String value, {bool allowMalformed = true}); - - /// Returns the written bytes as a [Uint8List] and resets the writer. - /// - /// This method returns a copy of the written bytes from the beginning to the - /// current offset position. After returning the bytes, it resets the internal - /// state by clearing the offset and reinitializing the buffer to its initial - /// size, preparing the writer for new data. - /// - /// Use this method when you want to retrieve the data and start fresh. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// final bytes = writer.takeBytes(); // Returns [42] and resets the writer - /// writer.writeUint8(100); // Can write new data - /// ``` - Uint8List takeBytes(); - - /// Returns the written bytes as a [Uint8List] without resetting the writer. - /// - /// This method returns a view of the written bytes from the beginning to the - /// current offset position. Unlike [takeBytes], this method does not reset - /// the internal state, allowing you to continue writing more data. - /// - /// Use this method when you want to inspect the current buffer state without - /// losing the ability to continue writing. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// final bytes = writer.toBytes(); // Returns [42] without resetting - /// writer.writeUint8(100); // Continues writing, buffer is now [42, 100] - /// ``` - Uint8List toBytes(); - - /// Resets the writer to its initial state. - /// - /// This method resets the offset to 0 and reinitializes the buffer to its - /// initial size. Unlike [takeBytes], this method does not return the written - /// bytes, making it useful when you want to discard the current data and - /// start fresh. - /// - /// Use this method when you want to clear the buffer without retrieving data. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// writer.reset(); // Resets the writer without returning bytes - /// writer.writeUint8(100); // Starts fresh with new data - /// ``` - void reset(); -} diff --git a/lib/src/constants_native.dart b/lib/src/constants_native.dart new file mode 100644 index 0000000..13984d3 --- /dev/null +++ b/lib/src/constants_native.dart @@ -0,0 +1,2 @@ +const kMaxInt64 = 9223372036854775807; +const kMinInt64 = -9223372036854775808; diff --git a/lib/src/constants_web.dart b/lib/src/constants_web.dart new file mode 100644 index 0000000..b1f9266 --- /dev/null +++ b/lib/src/constants_web.dart @@ -0,0 +1,2 @@ +const kMaxInt64 = 9007199254740991; // Max Safe Integer for JavaScript +const kMinInt64 = -9007199254740991; diff --git a/pubspec.yaml b/pubspec.yaml index c955e15..dd29268 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: 2.2.0 +version: 3.0.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues @@ -28,4 +28,6 @@ dev_dependencies: benchmark_harness: ^2.4.0 pro_lints: ^5.1.0 test: ^1.28.0 +dependencies: + meta: ^1.17.0 diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart deleted file mode 100644 index cc59edc..0000000 --- a/test/binary_reader_performance_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:typed_data'; - -import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:pro_binary/pro_binary.dart'; - -class BinaryReaderBenchmark extends BenchmarkBase { - BinaryReaderBenchmark() : super('BinaryReader performance test'); - - late final BinaryReader reader; - - @override - void setup() { - const string = 'Hello, World!'; - const longString = - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.'; - - final writer = BinaryWriter() - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14, Endian.little) - ..writeFloat64(3.141592653589793, Endian.little) - ..writeFloat64(2.718281828459045) - ..writeInt8(string.length) - ..writeString(string) - ..writeInt32(longString.length) - ..writeString(longString) - ..writeBytes([]) - ..writeBytes(List.filled(120, 100)); - - final buffer = writer.takeBytes(); - reader = BinaryReader(buffer); - } - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final _ = reader.readUint8(); - final _ = reader.readInt8(); - final _ = reader.readUint16(Endian.little); - final _ = reader.readInt16(Endian.little); - final _ = reader.readUint32(Endian.little); - final _ = reader.readInt32(Endian.little); - final _ = reader.readUint64(Endian.little); - final _ = reader.readInt64(Endian.little); - final _ = reader.readFloat32(Endian.little); - final _ = reader.readFloat64(Endian.little); - final _ = reader.readFloat64(Endian.little); - final length = reader.readInt8(); - final _ = reader.readString(length); - final longLength = reader.readInt32(); - final _ = reader.readString(longLength); - final _ = reader.readBytes(0); - final _ = reader.readBytes(120); - - assert(reader.availableBytes == 0, 'Not all bytes were read'); - reader.reset(); - } - } - - static void main() { - BinaryReaderBenchmark().report(); - } -} - -void main() { - BinaryReaderBenchmark.main(); -} diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart deleted file mode 100644 index a122d81..0000000 --- a/test/binary_reader_test.dart +++ /dev/null @@ -1,969 +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('readUint8', () { - final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8(), equals(1)); - expect(reader.availableBytes, equals(0)); - }); - - test('readInt8', () { - final buffer = Uint8List.fromList([0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt8(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('readUint16 big-endian', () { - final buffer = Uint8List.fromList([0x01, 0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(256)); - expect(reader.availableBytes, equals(0)); - }); - - test('readUint16 little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(Endian.little), equals(256)); - expect(reader.availableBytes, equals(0)); - }); - - test('readInt16 big-endian', () { - final buffer = Uint8List.fromList([0xFF, 0xFF]); - final reader = BinaryReader(buffer); - - expect(reader.readInt16(), equals(-1)); - expect(reader.availableBytes, equals(0)); - }); - - test('readInt16 little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x80]); - final reader = BinaryReader(buffer); - - expect(reader.readInt16(Endian.little), equals(-32768)); - expect(reader.availableBytes, equals(0)); - }); - - test('readUint32 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('readUint32 little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x00, 0x01, 0x00]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32(Endian.little), equals(65536)); - expect(reader.availableBytes, equals(0)); - }); - - test('readInt32 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('readInt32 little-endian', () { - final buffer = Uint8List.fromList([0x00, 0x00, 0x00, 0x80]); - final reader = BinaryReader(buffer); - - expect(reader.readInt32(Endian.little), equals(-2147483648)); - expect(reader.availableBytes, equals(0)); - }); - - test('readUint64 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('readUint64 little-endian', () { - final buffer = Uint8List.fromList([ - 0x00, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readUint64(Endian.little), equals(4294967296)); - expect(reader.availableBytes, equals(0)); - }); - - test('readInt64 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('readInt64 little-endian', () { - final buffer = Uint8List.fromList([ - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x80, - ]); - final reader = BinaryReader(buffer); - - expect(reader.readInt64(Endian.little), equals(-9223372036854775808)); - expect(reader.availableBytes, equals(0)); - }); - - test('readFloat32 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('readFloat32 little-endian', () { - final buffer = Uint8List.fromList([0xDB, 0x0F, 0x49, 0x40]); // 3.1415927 - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(Endian.little), closeTo(3.1415927, 0.0000001)); - expect(reader.availableBytes, equals(0)); - }); - - test('readFloat64 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('readFloat64 little-endian', () { - final buffer = Uint8List.fromList([ - 0x18, - 0x2D, - 0x44, - 0x54, - 0xFB, - 0x21, - 0x09, - 0x40, - ]); // 3.141592653589793 - final reader = BinaryReader(buffer); - - expect( - reader.readFloat64(Endian.little), - closeTo(3.141592653589793, 0.000000000000001), - ); - expect(reader.availableBytes, equals(0)); - }); - - 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('usedBytes returns correct number of used bytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); - - expect(reader.usedBytes, equals(0)); - reader.readUint8(); - expect(reader.usedBytes, equals(1)); - reader.readBytes(2); - expect(reader.usedBytes, equals(3)); - }); - - 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.usedBytes, equals(0)); - - reader.readUint8(); // Now usedBytes should be 1 - final peekedBytesWithOffset = reader.peekBytes(2, 2); - expect(peekedBytesWithOffset, equals([0x30, 0x40])); - expect(reader.usedBytes, 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.usedBytes, equals(2)); - expect(reader.readUint8(), equals(0x02)); - }); - - test('read zero-length bytes', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readBytes(0), equals([])); - expect(reader.availableBytes, equals(0)); - }); - - test('read beyond buffer throws AssertionError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(reader.readUint32, throwsA(isA())); - }); - - test('negative length input throws AssertionError', () { - 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 AssertionError', () { - 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 AssertionError', () { - final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' - final reader = BinaryReader(buffer); - - expect(() => reader.readString(5), throwsA(isA())); - }); - - test('readBytes with insufficient bytes throws AssertionError', () { - final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); - - expect(() => reader.readBytes(3), throwsA(isA())); - }); - - test('read methods throw AssertionError 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 AssertionError', - () { - 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 AssertionError', () { - 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)); - }); - - test('readString with UTF-8 multi-byte characters', () { - const str = 'こんにちは世界'; // "Hello, World" in Japanese - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - }); - - 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)); - }); - - test('offset equals usedBytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); - expect(reader.offset, equals(reader.usedBytes)); - - reader.readUint8(); - expect(reader.offset, equals(reader.usedBytes)); - }); - - test('offset resets to 0 after reset', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); - expect(reader.offset, equals(1)); - - reader.reset(); - expect(reader.offset, equals(0)); - }); - }); - - 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, double.nan); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('readFloat32 with Infinity', () { - final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, double.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, double.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, double.nan); - final reader = BinaryReader(buffer); - - expect(reader.readFloat64().isNaN, isTrue); - }); - - test('readFloat64 with Infinity', () { - final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, double.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, double.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)); - }); - - test('multiple resets in sequence', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer) - ..readUint8() - ..reset() - ..reset() - ..reset(); - - expect(reader.offset, equals(0)); - expect(reader.availableBytes, equals(3)); - }); - - test('read after buffer exhaustion and reset', () { - final buffer = Uint8List.fromList([0x42, 0x43]); - final reader = BinaryReader(buffer); - - expect(reader.readUint8(), equals(0x42)); - expect(reader.readUint8(), equals(0x43)); - expect(reader.availableBytes, equals(0)); - - reader.reset(); - expect(reader.readUint8(), equals(0x42)); - }); - }); - - 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('multiple reset calls with intermediate reads', () { - final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - 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)); - }); - - 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, Endian.little) - ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, Endian.little); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(Endian.little), equals(0x5678)); - expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(Endian.little), equals(0x11223344)); - }); - - test('float values with different endianness', () { - final writer = BinaryWriter() - ..writeFloat32(3.14) - ..writeFloat32(2.71, Endian.little) - ..writeFloat64(1.414) - ..writeFloat64(1.732, Endian.little); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(Endian.little), closeTo(2.71, 0.01)); - expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(Endian.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)); - }); - }); - }); -} diff --git a/test/binary_writer_performance_test.dart b/test/binary_writer_performance_test.dart deleted file mode 100644 index 8fc2d79..0000000 --- a/test/binary_writer_performance_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:typed_data'; - -import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:pro_binary/pro_binary.dart'; - -class BinaryWriterBenchmark extends BenchmarkBase { - BinaryWriterBenchmark() : super('BinaryWriter performance test'); - - late final BinaryWriter writer; - - @override - void setup() { - writer = BinaryWriter(); - } - - @override - void run() { - for (var i = 0; i < 1000; i++) { - writer - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14, Endian.little) - ..writeFloat64(3.141592653589793, Endian.little) - ..writeBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255]) - ..writeString('Hello, World!') - ..writeString( - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.', - ); - - final _ = writer.takeBytes(); - } - } - - @override - void exercise() => run(); - static void main() { - BinaryWriterBenchmark().report(); - } -} - -void main() { - BinaryWriterBenchmark.main(); -} diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart deleted file mode 100644 index 0545048..0000000 --- a/test/binary_writer_test.dart +++ /dev/null @@ -1,1330 +0,0 @@ -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('should return empty list when takeBytes called on empty writer', () { - expect(writer.takeBytes(), isEmpty); - }); - - test('should write single Uint8 value correctly', () { - writer.writeUint8(1); - expect(writer.takeBytes(), [1]); - }); - - test('should write negative Int8 value correctly', () { - writer.writeInt8(-1); - expect(writer.takeBytes(), [255]); - }); - - test('should write Uint16 in big-endian format', () { - writer.writeUint16(256); - expect(writer.takeBytes(), [1, 0]); - }); - - test('should write Uint16 in little-endian format', () { - writer.writeUint16(256, Endian.little); - expect(writer.takeBytes(), [0, 1]); - }); - - test('should write Int16 in big-endian format', () { - writer.writeInt16(-1); - expect(writer.takeBytes(), [255, 255]); - }); - - test('should write Int16 in little-endian format', () { - writer.writeInt16(-32768, Endian.little); - expect(writer.takeBytes(), [0, 128]); - }); - - test('should write Uint32 in big-endian format', () { - writer.writeUint32(65536); - expect(writer.takeBytes(), [0, 1, 0, 0]); - }); - - test('should write Uint32 in little-endian format', () { - writer.writeUint32(65536, Endian.little); - expect(writer.takeBytes(), [0, 0, 1, 0]); - }); - - test('should write Int32 in big-endian format', () { - writer.writeInt32(-1); - expect(writer.takeBytes(), [255, 255, 255, 255]); - }); - - test('should write Int32 in little-endian format', () { - writer.writeInt32(-2147483648, Endian.little); - expect(writer.takeBytes(), [0, 0, 0, 128]); - }); - - test('should write Uint64 in big-endian format', () { - writer.writeUint64(4294967296); - expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); - }); - - test('should write Uint64 in little-endian format', () { - writer.writeUint64(4294967296, Endian.little); - expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); - }); - - test('should write Int64 in big-endian format', () { - writer.writeInt64(-1); - expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); - }); - - test('should write Int64 in little-endian format', () { - writer.writeInt64(-9223372036854775808, Endian.little); - expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); - }); - - test('should write Float32 in big-endian format', () { - writer.writeFloat32(3.1415927); - expect(writer.takeBytes(), [64, 73, 15, 219]); - }); - - test('should write Float32 in little-endian format', () { - writer.writeFloat32(3.1415927, Endian.little); - expect(writer.takeBytes(), [219, 15, 73, 64]); - }); - - test('should write Float64 in big-endian format', () { - writer.writeFloat64(3.141592653589793); - expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); - }); - - test('should write Float64 in little-endian format', () { - writer.writeFloat64(3.141592653589793, Endian.little); - expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); - }); - - test('should write byte array correctly', () { - writer.writeBytes([1, 2, 3, 4, 5]); - expect(writer.takeBytes(), [1, 2, 3, 4, 5]); - }); - - test('should 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('should 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('should allow reusing writer after takeBytes', () { - writer.writeUint8(1); - expect(writer.takeBytes(), [1]); - - writer.writeUint8(2); - expect(writer.takeBytes(), [2]); - }); - - test('should 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('should 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)); - }); - - group('Input validation', () { - test('should throw AssertionError 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('should throw AssertionError 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('should throw AssertionError 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('should throw AssertionError 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('should throw AssertionError 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('should throw AssertionError 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 AssertionError 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('should throw AssertionError 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('should throw AssertionError 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 AssertionError 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 AssertionError 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 AssertionError 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('should 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('should return empty list when called on empty writer', () { - final bytes = writer.toBytes(); - expect(bytes, isEmpty); - }); - }); - - group('clear', () { - test('should reset writer state without returning bytes', () { - writer - ..writeUint8(42) - ..writeUint8(100) - ..reset(); - - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('should allow writing new data after reset', () { - writer - ..writeUint8(42) - ..reset() - ..writeUint8(100); - - expect(writer.toBytes(), equals([100])); - }); - - test('should be safe to call on empty writer', () { - writer.reset(); - expect(writer.bytesWritten, equals(0)); - }); - }); - - group('Edge cases', () { - test('should handle empty string correctly', () { - writer.writeString(''); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('should handle empty byte array correctly', () { - writer.writeBytes([]); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); - }); - - test('should 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('should handle Float32 NaN value correctly', () { - writer.writeFloat32(double.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32().isNaN, isTrue); - }); - - test('should handle Float32 positive Infinity correctly', () { - writer.writeFloat32(double.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.infinity)); - }); - - test('should handle Float32 negative Infinity correctly', () { - writer.writeFloat32(double.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat32(), equals(double.negativeInfinity)); - }); - - test('should handle Float64 NaN value correctly', () { - writer.writeFloat64(double.nan); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64().isNaN, isTrue); - }); - - test('should handle Float64 positive Infinity correctly', () { - writer.writeFloat64(double.infinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.infinity)); - }); - - test('should handle Float64 negative Infinity correctly', () { - writer.writeFloat64(double.negativeInfinity); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readFloat64(), equals(double.negativeInfinity)); - }); - - test('should 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('should throw AssertionError 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('should handle multiple consecutive reset calls', () { - writer - ..writeUint8(42) - ..reset() - ..reset() - ..reset(); - - expect(writer.bytesWritten, equals(0)); - }); - - test('should support method chaining after reset', () { - writer - ..writeUint8(1) - ..reset() - ..writeUint8(2) - ..writeUint8(3); - - expect(writer.toBytes(), equals([2, 3])); - }); - }); - - group('Boundary values - Maximum', () { - test('should handle Uint8 maximum value (255)', () { - writer.writeUint8(255); - expect(writer.takeBytes(), equals([255])); - }); - - test('should handle Int8 maximum positive value (127)', () { - writer.writeInt8(127); - expect(writer.takeBytes(), equals([127])); - }); - - test('should handle Int8 minimum negative value (-128)', () { - writer.writeInt8(-128); - expect(writer.takeBytes(), equals([128])); - }); - - test('should handle Uint16 maximum value (65535)', () { - writer.writeUint16(65535); - expect(writer.takeBytes(), equals([255, 255])); - }); - - test('should handle Int16 maximum positive value (32767)', () { - writer.writeInt16(32767); - expect(writer.takeBytes(), equals([127, 255])); - }); - - test('should handle Uint32 maximum value (4294967295)', () { - writer.writeUint32(4294967295); - expect(writer.takeBytes(), equals([255, 255, 255, 255])); - }); - - test('should handle Int32 maximum positive value (2147483647)', () { - writer.writeInt32(2147483647); - expect(writer.takeBytes(), equals([127, 255, 255, 255])); - }); - - test('should 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('should handle Uint8 minimum value (0)', () { - writer.writeUint8(0); - expect(writer.takeBytes(), equals([0])); - }); - - test('should handle Int8 zero value', () { - writer.writeInt8(0); - expect(writer.takeBytes(), equals([0])); - }); - - test('should handle Uint16 minimum value (0)', () { - writer.writeUint16(0); - expect(writer.takeBytes(), equals([0, 0])); - }); - - test('should handle Int16 zero value', () { - writer.writeInt16(0); - expect(writer.takeBytes(), equals([0, 0])); - }); - - test('should handle Uint32 minimum value (0)', () { - writer.writeUint32(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0])); - }); - - test('should handle Int32 zero value', () { - writer.writeInt32(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0])); - }); - - test('should handle Uint64 minimum value (0)', () { - writer.writeUint64(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); - }); - - test('should handle Int64 zero value', () { - writer.writeInt64(0); - expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); - }); - }); - - group('Multiple operations', () { - test('should 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('should 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('should 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('should 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('should 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('should 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])); - }); - }); - - group('Float precision', () { - test('should 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('should 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('should 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('should 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('should encode ASCII characters correctly', () { - writer.writeString('ABC123'); - expect(writer.takeBytes(), equals([65, 66, 67, 49, 50, 51])); - }); - - test('should encode Cyrillic characters correctly', () { - writer.writeString('Привет'); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readString(bytes.length), equals('Привет')); - }); - - test('should 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('should 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('should 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('should 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, Endian.little); - final bytes = writer.takeBytes(); - - final reader = BinaryReader(bytes); - expect(reader.readUint64(Endian.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, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648) - ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14159, Endian.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(Endian.little), equals(-32768)); - expect(reader.readUint32(Endian.little), equals(4294967295)); - expect(reader.readInt32(), equals(-2147483648)); - expect(reader.readUint64(), equals(9223372036854775807)); - expect(reader.readInt64(Endian.little), equals(-9223372036854775808)); - expect(reader.readFloat32(Endian.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('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/integration_test.dart b/test/integration/integration_test.dart similarity index 59% rename from test/integration_test.dart rename to test/integration/integration_test.dart index 5644cd1..7a6b8d8 100644 --- a/test/integration_test.dart +++ b/test/integration/integration_test.dart @@ -45,11 +45,11 @@ void main() { final writer = BinaryWriter(); const value = 65535; - writer.writeUint16(value, Endian.little); + writer.writeUint16(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint16(Endian.little), equals(value)); + expect(reader.readUint16(.little), equals(value)); }); test('write and read Int16 with big-endian', () { @@ -67,11 +67,11 @@ void main() { final writer = BinaryWriter(); const value = -32768; - writer.writeInt16(value, Endian.little); + writer.writeInt16(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt16(Endian.little), equals(value)); + expect(reader.readInt16(.little), equals(value)); }); test('write and read Uint32 with big-endian', () { @@ -89,11 +89,11 @@ void main() { final writer = BinaryWriter(); const value = 4294967295; - writer.writeUint32(value, Endian.little); + writer.writeUint32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint32(Endian.little), equals(value)); + expect(reader.readUint32(.little), equals(value)); }); test('write and read Int32 with big-endian', () { @@ -111,11 +111,11 @@ void main() { final writer = BinaryWriter(); const value = -2147483648; - writer.writeInt32(value, Endian.little); + writer.writeInt32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt32(Endian.little), equals(value)); + expect(reader.readInt32(.little), equals(value)); }); test('write and read Uint64 with big-endian', () { @@ -133,11 +133,11 @@ void main() { final writer = BinaryWriter(); const value = 9223372036854775807; - writer.writeUint64(value, Endian.little); + writer.writeUint64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint64(Endian.little), equals(value)); + expect(reader.readUint64(.little), equals(value)); }); test('write and read Int64 with big-endian', () { @@ -155,11 +155,11 @@ void main() { final writer = BinaryWriter(); const value = -9223372036854775808; - writer.writeInt64(value, Endian.little); + writer.writeInt64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt64(Endian.little), equals(value)); + expect(reader.readInt64(.little), equals(value)); }); test('write and read Float32 with big-endian', () { @@ -177,11 +177,11 @@ void main() { final writer = BinaryWriter(); const value = 3.14159; - writer.writeFloat32(value, Endian.little); + writer.writeFloat32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readFloat32(Endian.little), closeTo(value, 0.00001)); + expect(reader.readFloat32(.little), closeTo(value, 0.00001)); }); test('write and read Float64 with big-endian', () { @@ -199,12 +199,12 @@ void main() { final writer = BinaryWriter(); const value = 3.141592653589793; - writer.writeFloat64(value, Endian.little); + writer.writeFloat64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); expect( - reader.readFloat64(Endian.little), + reader.readFloat64(.little), closeTo(value, 0.000000000000001), ); }); @@ -243,25 +243,25 @@ void main() { test('write and read with mixed endianness', () { final writer = BinaryWriter() ..writeUint16(0x1234) - ..writeUint16(0x5678, Endian.little) + ..writeUint16(0x5678, .little) ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, Endian.little) + ..writeUint32(0x11223344, .little) ..writeFloat32(3.14) - ..writeFloat32(2.71, Endian.little) + ..writeFloat32(2.71, .little) ..writeFloat64(1.414) - ..writeFloat64(1.732, Endian.little); + ..writeFloat64(1.732, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(Endian.little), equals(0x5678)); + expect(reader.readUint16(.little), equals(0x5678)); expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(Endian.little), equals(0x11223344)); + expect(reader.readUint32(.little), equals(0x11223344)); expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(Endian.little), closeTo(2.71, 0.01)); + expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(Endian.little), closeTo(1.732, 0.001)); + expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); }); test('write and read bytes array', () { @@ -446,7 +446,7 @@ void main() { group('Float special values', () { test('write and read Float32 NaN', () { - final writer = BinaryWriter()..writeFloat32(double.nan); + final writer = BinaryWriter()..writeFloat32(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -454,7 +454,7 @@ void main() { }); test('write and read Float32 positive Infinity', () { - final writer = BinaryWriter()..writeFloat32(double.infinity); + final writer = BinaryWriter()..writeFloat32(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -462,7 +462,7 @@ void main() { }); test('write and read Float32 negative Infinity', () { - final writer = BinaryWriter()..writeFloat32(double.negativeInfinity); + final writer = BinaryWriter()..writeFloat32(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -470,7 +470,7 @@ void main() { }); test('write and read Float64 NaN', () { - final writer = BinaryWriter()..writeFloat64(double.nan); + final writer = BinaryWriter()..writeFloat64(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -478,7 +478,7 @@ void main() { }); test('write and read Float64 positive Infinity', () { - final writer = BinaryWriter()..writeFloat64(double.infinity); + final writer = BinaryWriter()..writeFloat64(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -486,7 +486,7 @@ void main() { }); test('write and read Float64 negative Infinity', () { - final writer = BinaryWriter()..writeFloat64(double.negativeInfinity); + final writer = BinaryWriter()..writeFloat64(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -507,12 +507,12 @@ void main() { 'write and read multiple special float values together', () { final writer = BinaryWriter() - ..writeFloat32(double.nan) - ..writeFloat32(double.infinity) - ..writeFloat32(double.negativeInfinity) - ..writeFloat64(double.nan) - ..writeFloat64(double.infinity) - ..writeFloat64(double.negativeInfinity); + ..writeFloat32(.nan) + ..writeFloat32(.infinity) + ..writeFloat32(.negativeInfinity) + ..writeFloat64(.nan) + ..writeFloat64(.infinity) + ..writeFloat64(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -560,10 +560,10 @@ void main() { final reader = BinaryReader(bytes); expect(reader.readUint32(), equals(100)); - expect(reader.usedBytes, equals(4)); + expect(reader.offset, equals(4)); reader.reset(); - expect(reader.usedBytes, equals(0)); + expect(reader.offset, equals(0)); expect(reader.readUint32(), equals(100)); }); @@ -665,7 +665,7 @@ void main() { }); group('Writer buffer management integration', () { - test('write causes buffer expansion and can be read correctly', () { + test('buffer expansion preserves data integrity', () { final writer = BinaryWriter(initialBufferSize: 4); for (var i = 0; i < 100; i++) { @@ -763,48 +763,48 @@ void main() { 'all types round-trip correctly with little-endian', () { final writer = BinaryWriter() - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(1.23456, Endian.little) - ..writeFloat64(1.2345678901234, Endian.little); + ..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(Endian.little), + reader.readUint16(.little), equals(65535), ); expect( - reader.readInt16(Endian.little), + reader.readInt16(.little), equals(-32768), ); expect( - reader.readUint32(Endian.little), + reader.readUint32(.little), equals(4294967295), ); expect( - reader.readInt32(Endian.little), + reader.readInt32(.little), equals(-2147483648), ); expect( - reader.readUint64(Endian.little), + reader.readUint64(.little), equals(9223372036854775807), ); expect( - reader.readInt64(Endian.little), + reader.readInt64(.little), equals(-9223372036854775808), ); expect( - reader.readFloat32(Endian.little), + reader.readFloat32(.little), closeTo(1.23456, 0.00001), ); expect( - reader.readFloat64(Endian.little), + reader.readFloat64(.little), closeTo(1.2345678901234, 0.0000001), ); expect(reader.availableBytes, equals(0)); @@ -921,5 +921,472 @@ void main() { } }); }); + + 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/performance/reader/binary_read_bench_test.dart b/test/performance/reader/binary_read_bench_test.dart new file mode 100644 index 0000000..61c3924 --- /dev/null +++ b/test/performance/reader/binary_read_bench_test.dart @@ -0,0 +1,506 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading small byte arrays (< 16 bytes) +/// +/// Small reads are common for fixed-size headers, checksums, and IDs. +class SmallBytesReadBenchmark extends BenchmarkBase { + SmallBytesReadBenchmark() : super('Bytes read: small (8 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(8); + } + reader.reset(); + } +} + +/// Benchmark for reading medium byte arrays (64 bytes) +class MediumBytesReadBenchmark extends BenchmarkBase { + MediumBytesReadBenchmark() : super('Bytes read: medium (64 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(64, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(64); + } + reader.reset(); + } +} + +/// Benchmark for reading large byte arrays (1 KB) +class LargeBytesReadBenchmark extends BenchmarkBase { + LargeBytesReadBenchmark() : super('Bytes read: large (1 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 1024 * 1024); + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(1024); + } + reader.reset(); + } +} + +/// Benchmark for reading very large byte arrays (64 KB) +class VeryLargeBytesReadBenchmark extends BenchmarkBase { + VeryLargeBytesReadBenchmark() : super('Bytes read: very large (64 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 10; i++) { + reader.readBytes(64 * 1024); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes (length-prefixed byte arrays) +class VarBytesSmallReadBenchmark extends BenchmarkBase { + VarBytesSmallReadBenchmark() : super('VarBytes read: small'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes with medium-sized data +class VarBytesMediumReadBenchmark extends BenchmarkBase { + VarBytesMediumReadBenchmark() : super('VarBytes read: medium'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes with large data +class VarBytesLargeReadBenchmark extends BenchmarkBase { + VarBytesLargeReadBenchmark() : super('VarBytes read: large'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading empty byte arrays +class EmptyBytesReadBenchmark extends BenchmarkBase { + EmptyBytesReadBenchmark() : super('Bytes read: empty'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeBytes([]); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(0); + } + reader.reset(); + } +} + +/// Benchmark for peeking at bytes without advancing position +class PeekBytesReadBenchmark extends BenchmarkBase { + PeekBytesReadBenchmark() : super('Bytes peek: 16 bytes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(16, (i) => i)); + + writer.writeBytes(data); + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.peekBytes(16); + } + // No reset needed - we're not advancing position + } +} + +/// Benchmark for reading remaining bytes +class ReadRemainingBytesReadBenchmark extends BenchmarkBase { + ReadRemainingBytesReadBenchmark() : super('readRemainingBytes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(1024); + } + reader.reset(); + } +} + +/// Benchmark for mixed-size byte reads (realistic scenario) +/// +/// Simulates reading a protocol with headers, payloads, and checksums. +class MixedBytesReadBenchmark extends BenchmarkBase { + MixedBytesReadBenchmark() : super('Bytes read: mixed sizes (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + // Simulate a protocol message: + // - Header (16 bytes) + // - Payload (variable: 64, 128, 256 bytes) + // - Checksum (4 bytes) + for (var i = 0; i < 1000; i++) { + final header = Uint8List.fromList(List.generate(16, (j) => j)); + final payload = Uint8List.fromList( + List.generate(64 + (i % 3) * 64, (j) => (j + i) % 256), + ); + final checksum = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]); + + writer + ..writeBytes(header) + ..writeBytes(payload) + ..writeBytes(checksum); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readBytes(16) // Header + ..readBytes(64 + (i % 3) * 64) // Payload + ..readBytes(4); // Checksum + } + reader.reset(); + } +} + +/// Benchmark for alternating small and large reads +class AlternatingBytesReadBenchmark extends BenchmarkBase { + AlternatingBytesReadBenchmark() : super('Bytes read: alternating sizes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final small = Uint8List.fromList([1, 2, 3, 4]); + final large = Uint8List.fromList(List.generate(512, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer + ..writeBytes(small) + ..writeBytes(large); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readBytes(4) + ..readBytes(512); + } + reader.reset(); + } +} + +/// Benchmark for sequential small reads +/// +/// Tests performance when reading many small chunks sequentially. +class SequentialSmallReadsReadBenchmark extends BenchmarkBase { + SequentialSmallReadsReadBenchmark() + : super('Bytes read: sequential small reads'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(1); + } + reader.reset(); + } +} + +/// Benchmark for reading with skip operations +class SkipAndReadBenchmark extends BenchmarkBase { + SkipAndReadBenchmark() : super('Bytes read: skip + read pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final data = Uint8List.fromList(List.generate(8, (j) => (i + j) % 256)); + final padding = Uint8List.fromList(List.generate(8, (_) => 0)); + writer + ..writeBytes(data) + ..writeBytes(padding); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readBytes(8) // Read data + ..skip(8); // Skip padding + } + reader.reset(); + } +} + +void main() { + test('Fixed-size reads benchmarks:', () { + EmptyBytesReadBenchmark().report(); + SmallBytesReadBenchmark().report(); + MediumBytesReadBenchmark().report(); + LargeBytesReadBenchmark().report(); + VeryLargeBytesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('VarBytes (length-prefixed) benchmarks:', () { + VarBytesSmallReadBenchmark().report(); + VarBytesMediumReadBenchmark().report(); + VarBytesLargeReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Special operations benchmarks:', () { + PeekBytesReadBenchmark().report(); + ReadRemainingBytesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios benchmarks:', () { + MixedBytesReadBenchmark().report(); + AlternatingBytesReadBenchmark().report(); + SequentialSmallReadsReadBenchmark().report(); + SkipAndReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/reader/fixed_int_read_bench_test.dart b/test/performance/reader/fixed_int_read_bench_test.dart new file mode 100644 index 0000000..6838728 --- /dev/null +++ b/test/performance/reader/fixed_int_read_bench_test.dart @@ -0,0 +1,530 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading Uint8 (1 byte unsigned) +/// +/// Most basic read operation - single byte access without endianness concerns. +/// Should be the fastest fixed-int read operation. +class Uint8ReadBenchmark extends BenchmarkBase { + Uint8ReadBenchmark() : super('Uint8 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint8(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int8 (1 byte signed) +class Int8ReadBenchmark extends BenchmarkBase { + Int8ReadBenchmark() : super('Int8 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeInt8((i % 256) - 128); // Range: -128 to 127 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt8(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint16 in big-endian format +class Uint16BigEndianReadBenchmark extends BenchmarkBase { + Uint16BigEndianReadBenchmark() : super('Uint16 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint16((i * 257) % 65536); // Varied values + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint16(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint16 in little-endian format +class Uint16LittleEndianReadBenchmark extends BenchmarkBase { + Uint16LittleEndianReadBenchmark() : super('Uint16 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint16((i * 257) % 65536, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint16(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int16 in big-endian format +class Int16BigEndianReadBenchmark extends BenchmarkBase { + Int16BigEndianReadBenchmark() : super('Int16 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i * 257) % 65536 - 32768); // Range: -32768 to 32767 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt16(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int16 in little-endian format +class Int16LittleEndianReadBenchmark extends BenchmarkBase { + Int16LittleEndianReadBenchmark() : super('Int16 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i * 257) % 65536 - 32768, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt16(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint32 in big-endian format +class Uint32BigEndianReadBenchmark extends BenchmarkBase { + Uint32BigEndianReadBenchmark() : super('Uint32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Uint32 values + for (var i = 0; i < 1000; i++) { + writer.writeUint32((i * 1000000 + i * 123) % 4294967296); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint32 in little-endian format +class Uint32LittleEndianReadBenchmark extends BenchmarkBase { + Uint32LittleEndianReadBenchmark() : super('Uint32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint32((i * 1000000 + i * 123) % 4294967296, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int32 in big-endian format +class Int32BigEndianReadBenchmark extends BenchmarkBase { + Int32BigEndianReadBenchmark() : super('Int32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Int32 values + for (var i = 0; i < 1000; i++) { + writer.writeInt32((i * 1000000 + i * 123) % 4294967296 - 2147483648); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int32 in little-endian format +class Int32LittleEndianReadBenchmark extends BenchmarkBase { + Int32LittleEndianReadBenchmark() : super('Int32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeInt32( + (i * 1000000 + i * 123) % 4294967296 - 2147483648, + .little, + ); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint64 in big-endian format +class Uint64BigEndianReadBenchmark extends BenchmarkBase { + Uint64BigEndianReadBenchmark() : super('Uint64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Uint64 values + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000000 + i * 12345); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint64 in little-endian format +class Uint64LittleEndianReadBenchmark extends BenchmarkBase { + Uint64LittleEndianReadBenchmark() : super('Uint64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Uint64 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000000 + i * 12345, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int64 in big-endian format +class Int64BigEndianReadBenchmark extends BenchmarkBase { + Int64BigEndianReadBenchmark() : super('Int64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Int64 values + for (var i = 0; i < 1000; i++) { + final value = i.isEven + ? (i * 1000000000 + i * 12345) + : -(i * 1000000000 + i * 12345); + writer.writeInt64(value); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int64 in little-endian format +class Int64LittleEndianReadBenchmark extends BenchmarkBase { + Int64LittleEndianReadBenchmark() : super('Int64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Int64 values in little-endian + for (var i = 0; i < 1000; i++) { + final value = i.isEven + ? (i * 1000000000 + i * 12345) + : -(i * 1000000000 + i * 12345); + writer.writeInt64(value, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed fixed-width integers (realistic scenario) +/// +/// Simulates real-world protocol where various integer sizes are mixed. +/// Uses little-endian as it's more common in modern protocols. +class MixedFixedIntReadBenchmark extends BenchmarkBase { + MixedFixedIntReadBenchmark() : super('Mixed fixed-int read (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + for (var i = 0; i < 1000; i++) { + writer + ..writeUint8(127) // Message type + ..writeUint16(10, .little) // Length + ..writeUint32(1000, .little) // ID + ..writeInt32(-100, .little) // Signed value + ..writeUint64(1000000000, .little) // Timestamp + ..writeInt8(64) // Small signed value + ..writeInt16(-1000, .little) // Medium signed value + ..writeInt64(-10000000, .little); // Large signed value + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readUint8() + ..readUint16(.little) + ..readUint32(.little) + ..readInt32(.little) + ..readUint64(.little) + ..readInt8() + ..readInt16(.little) + ..readInt64(.little); + } + reader.reset(); + } +} + +void main() { + test('8-bit integer benchmarks:', () { + Uint8ReadBenchmark().report(); + Int8ReadBenchmark().report(); + }, tags: ['benchmark']); + + test('16-bit integer benchmarks:', () { + Uint16BigEndianReadBenchmark().report(); + Uint16LittleEndianReadBenchmark().report(); + Int16BigEndianReadBenchmark().report(); + Int16LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('32-bit integer benchmarks:', () { + Uint32BigEndianReadBenchmark().report(); + Uint32LittleEndianReadBenchmark().report(); + Int32BigEndianReadBenchmark().report(); + Int32LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('64-bit integer benchmarks:', () { + Uint64BigEndianReadBenchmark().report(); + Uint64LittleEndianReadBenchmark().report(); + Int64BigEndianReadBenchmark().report(); + Int64LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed integer benchmarks:', () { + MixedFixedIntReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/reader/float_read_bench_test.dart b/test/performance/reader/float_read_bench_test.dart new file mode 100644 index 0000000..69f187b --- /dev/null +++ b/test/performance/reader/float_read_bench_test.dart @@ -0,0 +1,466 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading Float32 in big-endian format +/// +/// Float32 (IEEE 754 single precision) is commonly used for graphics, +/// game data, and scientific computing where memory efficiency matters. +class Float32BigEndianReadBenchmark extends BenchmarkBase { + Float32BigEndianReadBenchmark() : super('Float32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + final value = (i * 3.14159) - 500.0; + writer.writeFloat32(value); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 in little-endian format +class Float32LittleEndianReadBenchmark extends BenchmarkBase { + Float32LittleEndianReadBenchmark() : super('Float32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 Float32 values in little-endian + for (var i = 0; i < 1000; i++) { + final value = (i * 3.14159) - 500.0; + writer.writeFloat32(value, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 in big-endian format +/// +/// Float64 (IEEE 754 double precision) is the default floating-point type +/// in Dart and most high-level languages. Used for general-purpose math. +class Float64BigEndianReadBenchmark extends BenchmarkBase { + Float64BigEndianReadBenchmark() : super('Float64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final value = (i * 2.718281828) - 1000.0; + writer.writeFloat64(value); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 in little-endian format +class Float64LittleEndianReadBenchmark extends BenchmarkBase { + Float64LittleEndianReadBenchmark() : super('Float64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final value = (i * 2.718281828) - 1000.0; + writer.writeFloat64(value, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 special values (NaN, Infinity) +/// +/// Special IEEE 754 values may have different performance characteristics +/// due to how hardware handles them. +class Float32SpecialValuesReadBenchmark extends BenchmarkBase { + Float32SpecialValuesReadBenchmark() : super('Float32 read (special values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 200; i++) { + writer + ..writeFloat32(.nan, .little) + ..writeFloat32(.infinity, .little) + ..writeFloat32(.negativeInfinity, .little) + ..writeFloat32(-0, .little) + ..writeFloat32(1, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 special values (NaN, Infinity) +class Float64SpecialValuesReadBenchmark extends BenchmarkBase { + Float64SpecialValuesReadBenchmark() : super('Float64 read (special values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 200; i++) { + writer + ..writeFloat64(.nan, .little) + ..writeFloat64(.infinity, .little) + ..writeFloat64(.negativeInfinity, .little) + ..writeFloat64(-0, .little) + ..writeFloat64(1, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 with small values (subnormal range) +/// +/// Subnormal numbers (very close to zero) may have different performance. +class Float32SmallValuesReadBenchmark extends BenchmarkBase { + Float32SmallValuesReadBenchmark() : super('Float32 read (small values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e-38; // Near Float32 min positive normal + writer.writeFloat32(value, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 with small values (subnormal range) +class Float64SmallValuesReadBenchmark extends BenchmarkBase { + Float64SmallValuesReadBenchmark() : super('Float64 read (small values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e-308; // Near Float64 min positive normal + writer.writeFloat64(value, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 with large values +class Float32LargeValuesReadBenchmark extends BenchmarkBase { + Float32LargeValuesReadBenchmark() : super('Float32 read (large values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e35; // Near Float32 max (~3.4e38) + writer.writeFloat32(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 with large values +class Float64LargeValuesReadBenchmark extends BenchmarkBase { + Float64LargeValuesReadBenchmark() : super('Float64 read (large values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e305; // Near Float64 max (~1.8e308) + writer.writeFloat64(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed Float32 and Float64 (realistic scenario) +/// +/// Simulates real-world usage where both precision levels are used. +/// For example: positions (Float32) + precise calculations (Float64). +class MixedFloatReadBenchmark extends BenchmarkBase { + MixedFloatReadBenchmark() : super('Mixed float read (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 100; i++) { + writer + // 3D position (Float32 x3) + ..writeFloat32(i * 1.5, .little) + ..writeFloat32(i * 2.0, .little) + ..writeFloat32(i * 0.5, .little) + // Rotation quaternion (Float32 x4) + ..writeFloat32(0.707, .little) + ..writeFloat32(0, .little) + ..writeFloat32(0.707, .little) + ..writeFloat32(0, .little) + // Precise timestamp (Float64) + ..writeFloat64(i * 1000000.0, .little) + // Color (Float32 x4 - RGBA) + ..writeFloat32(0.5, .little) + ..writeFloat32(0.8, .little) + ..writeFloat32(0.2, .little) + ..writeFloat32(1, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + // Read position + reader + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + // Read rotation + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + // Read timestamp + ..readFloat64(.little) + // Read color + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading alternating Float32/Float64 +/// +/// Tests performance when switching between 32-bit and 64-bit reads. +class AlternatingFloatReadBenchmark extends BenchmarkBase { + AlternatingFloatReadBenchmark() : super('Alternating Float32/Float64 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 500; i++) { + writer + ..writeFloat32(i * 3.14, .little) + ..writeFloat64(i * 2.718, .little); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 500; i++) { + reader + ..readFloat32(.little) + ..readFloat64(.little); + } + reader.reset(); + } +} + +void main() { + test('Float32 benchmarks:', () { + Float32BigEndianReadBenchmark().report(); + Float32LittleEndianReadBenchmark().report(); + Float32SmallValuesReadBenchmark().report(); + Float32LargeValuesReadBenchmark().report(); + Float32SpecialValuesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Float64 benchmarks:', () { + Float64BigEndianReadBenchmark().report(); + Float64LittleEndianReadBenchmark().report(); + Float64SmallValuesReadBenchmark().report(); + Float64LargeValuesReadBenchmark().report(); + Float64SpecialValuesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed float benchmarks:', () { + MixedFloatReadBenchmark().report(); + AlternatingFloatReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/reader/navigation_bench_test.dart b/test/performance/reader/navigation_bench_test.dart new file mode 100644 index 0000000..9364117 --- /dev/null +++ b/test/performance/reader/navigation_bench_test.dart @@ -0,0 +1,535 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for skip operations (small offsets) +/// +/// Skip is commonly used to jump over padding, unused fields, or known +/// sections. +class SkipSmallOffsetBenchmark extends BenchmarkBase { + SkipSmallOffsetBenchmark() : super('Skip: small offset (8 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for skip operations (medium offsets) +class SkipMediumOffsetBenchmark extends BenchmarkBase { + SkipMediumOffsetBenchmark() : super('Skip: medium offset (256 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(256); + } + reader.reset(); + } +} + +/// Benchmark for skip operations (large offsets) +class SkipLargeOffsetBenchmark extends BenchmarkBase { + SkipLargeOffsetBenchmark() : super('Skip: large offset (4 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + // Write 1000 chunks of 4KB + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(4096); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (forward) +/// +/// Seek is used for random access patterns, like jumping to specific offsets. +class SeekForwardBenchmark extends BenchmarkBase { + SeekForwardBenchmark() : super('Seek: forward (sequential positions)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 100KB of data + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + // Seek to 1000 different positions + for (var i = 0; i < 1000; i++) { + reader.seek((i * 100) % 90000); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (backward) +class SeekBackwardBenchmark extends BenchmarkBase { + SeekBackwardBenchmark() : super('Seek: backward (reverse positions)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + + reader.seek(90000); // Start near end + } + + @override + void exercise() => run(); + + @override + void run() { + // Seek backward to 1000 different positions + for (var i = 1000; i > 0; i--) { + reader.seek((i * 90) % 90000); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (random access) +class SeekRandomAccessBenchmark extends BenchmarkBase { + SeekRandomAccessBenchmark() : super('Seek: random access pattern'); + + late BinaryReader reader; + late Uint8List buffer; + late List positions; + + @override + void setup() { + final writer = BinaryWriter(); + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + + writer.writeBytes(data); + buffer = writer.takeBytes(); + + reader = BinaryReader(buffer); + // Pre-calculate random-like positions (deterministic for consistency) + positions = List.generate(1000, (i) => (i * 7919) % 90000); + } + + @override + void exercise() => run(); + + @override + void run() { + // Disable lint for using for-in to emphasize the benchmark nature + // ignore: prefer_foreach + for (final pos in positions) { + reader.seek(pos); + } + reader.reset(); + } +} + +/// Benchmark for rewind operations +/// +/// Rewind resets position to the beginning - common in parsing retry scenarios. +class RewindBenchmark extends BenchmarkBase { + RewindBenchmark() : super('Rewind: reset to start'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(8) + ..reset(); + } + } +} + +/// Benchmark for reset operations +/// +/// Reset is similar to rewind - tests the efficiency of position reset. +class ResetBenchmark extends BenchmarkBase { + ResetBenchmark() : super('Reset: position reset'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(8) + ..reset(); + } + } +} + +/// Benchmark for getPosition operations +/// +/// Getting current position (offset) is often needed in parsing to track +/// offsets. +class GetPositionBenchmark extends BenchmarkBase { + GetPositionBenchmark() : super('offset: query current position'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for remainingBytes getter +class RemainingBytesBenchmark extends BenchmarkBase { + RemainingBytesBenchmark() : super('availableBytes: query remaining length'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for combined navigation operations (realistic parsing) +/// +/// Simulates a parser that needs to: +/// 1. Check position +/// 2. Peek at header +/// 3. Decide to skip or read +/// 4. Move to next section +class RealisticParsingNavigationBenchmark extends BenchmarkBase { + RealisticParsingNavigationBenchmark() + : super('Navigation: realistic parsing pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write protocol-like data: header (4 bytes) + payload (variable) + for (var i = 0; i < 1000; i++) { + final payloadSize = 16 + (i % 8) * 8; + writer + ..writeUint32(payloadSize) // Header with payload size + ..writeBytes(List.generate(payloadSize, (j) => (i + j) % 256)); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + // 1. Get current position + reader.offset; + // 2. Peek at header to determine payload size + final peekData = reader.peekBytes(4); + final payloadSize = ByteData.view(peekData.buffer).getUint32(0); + // 3. Skip header + reader.skip(4); + // 4. Decide: skip payload based on some condition + if (i % 3 == 0) { + reader.skip(payloadSize); + } else { + // Read and process payload + reader.readBytes(payloadSize); + } + } + reader.reset(); + } +} + +/// Benchmark for seek + read pattern +/// +/// Common in binary file formats with indexes or tables of contents. +class SeekAndReadBenchmark extends BenchmarkBase { + SeekAndReadBenchmark() : super('Navigation: seek + read pattern'); + + late BinaryReader reader; + late Uint8List buffer; + late List offsets; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 100 records of 64 bytes each + offsets = []; + for (var i = 0; i < 100; i++) { + offsets.add(i * 64); // Track offsets manually + final data = Uint8List.fromList(List.generate(64, (j) => (i + j) % 256)); + writer.writeBytes(data); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + // Read records in non-sequential order + for (var i = 0; i < 100; i++) { + final idx = (i * 7) % 100; + reader + ..seek(offsets[idx]) + ..readBytes(64); + } + reader.reset(); + } +} + +/// Benchmark for skip + peek pattern +/// +/// Used when scanning through data looking for specific patterns. +class SkipAndPeekBenchmark extends BenchmarkBase { + SkipAndPeekBenchmark() : super('Navigation: skip + peek pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write pattern: 4 bytes to skip, 4 bytes to peek + for (var i = 0; i < 1000; i++) { + writer + ..writeUint32(0xDEADBEEF) // Skip this + ..writeUint32(i); // Peek at this + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(4) + ..peekBytes(4) + ..skip(4); + } + reader.reset(); + } +} + +/// Benchmark for backward navigation (seek back and re-read) +/// +/// Used when parser needs to backtrack. +class BacktrackNavigationBenchmark extends BenchmarkBase { + BacktrackNavigationBenchmark() : super('Navigation: backtrack pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + for (var i = 0; i < 2000; i++) { + writer.writeUint32(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 500; i++) { + // Read forward + reader + ..readUint32() + ..readUint32(); + final pos = reader.offset; + reader + ..readUint32() + // Backtrack to saved position + ..seek(pos) + // Re-read + ..readUint32(); + } + reader.reset(); + } +} + +void main() { + test('Skip operation benchmarks:', () { + SkipSmallOffsetBenchmark().report(); + SkipMediumOffsetBenchmark().report(); + SkipLargeOffsetBenchmark().report(); + }, tags: ['benchmark']); + + test('Seek operation benchmarks:', () { + SeekForwardBenchmark().report(); + SeekBackwardBenchmark().report(); + SeekRandomAccessBenchmark().report(); + }, tags: ['benchmark']); + + test('Position control benchmarks:', () { + RewindBenchmark().report(); + ResetBenchmark().report(); + GetPositionBenchmark().report(); + }, tags: ['benchmark']); + + test('Position query benchmarks:', () { + GetPositionBenchmark().report(); + RemainingBytesBenchmark().report(); + }, tags: ['benchmark']); + + test('Complex navigation patterns:', () { + RealisticParsingNavigationBenchmark().report(); + SeekAndReadBenchmark().report(); + SkipAndPeekBenchmark().report(); + BacktrackNavigationBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/reader/string_read_bench_test.dart b/test/performance/reader/string_read_bench_test.dart new file mode 100644 index 0000000..e54ee4a --- /dev/null +++ b/test/performance/reader/string_read_bench_test.dart @@ -0,0 +1,522 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading ASCII strings (fast path) +/// +/// ASCII-only strings use the fast path in UTF-8 decoding, +/// processing multiple bytes at once. This is the most common case. +class AsciiStringReadBenchmark extends BenchmarkBase { + AsciiStringReadBenchmark() : super('String read: ASCII only'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(); + const asciiString = 'Hello, World! This is a test string 123456789'; + stringLength = asciiString.length; + + for (var i = 0; i < 1000; i++) { + writer.writeString(asciiString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +/// Benchmark for reading short ASCII strings (< 16 chars) +class ShortAsciiStringReadBenchmark extends BenchmarkBase { + ShortAsciiStringReadBenchmark() : super('String read: short ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + const strings = [ + 'Hi', + 'Test', + 'Hello', + 'OK', + 'Error', + 'Success', + '123', + 'ABC', + ]; + + // Write 1000 short strings + for (var i = 0; i < 1000; i++) { + strings.forEach(writer.writeString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + // Read in same pattern + for (var i = 0; i < 1000; i++) { + reader + ..readString(2) // Hi + ..readString(4) // Test + ..readString(5) // Hello + ..readString(2) // OK + ..readString(5) // Error + ..readString(7) // Success + ..readString(3) // 123 + ..readString(3); // ABC + } + reader.reset(); + } +} + +/// Benchmark for reading long ASCII strings (> 100 chars) +class LongAsciiStringReadBenchmark extends BenchmarkBase { + LongAsciiStringReadBenchmark() : super('String read: long ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(); + const longString = + 'The quick brown fox jumps over the lazy dog. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'; + stringLength = longString.length; + + // Write 1000 long ASCII strings + for (var i = 0; i < 1000; i++) { + writer.writeString(longString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +/// Benchmark for reading Cyrillic strings (2-byte UTF-8) +class CyrillicStringReadBenchmark extends BenchmarkBase { + CyrillicStringReadBenchmark() : super('String read: Cyrillic (2-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(); + const cyrillicString = 'Привет мир! Это тестовая строка на русском языке.'; + byteLength = getUtf8Length(cyrillicString); + + // Write 1000 Cyrillic strings + for (var i = 0; i < 1000; i++) { + writer.writeString(cyrillicString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading CJK strings (3-byte UTF-8) +class CjkStringReadBenchmark extends BenchmarkBase { + CjkStringReadBenchmark() : super('String read: CJK (3-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(); + const cjkString = '你好世界!这是一个测试字符串。日本語のテストも含まれています。'; + byteLength = getUtf8Length(cjkString); + + // Write 1000 CJK strings + for (var i = 0; i < 1000; i++) { + writer.writeString(cjkString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading emoji strings (4-byte UTF-8) +class EmojiStringReadBenchmark extends BenchmarkBase { + EmojiStringReadBenchmark() : super('String read: Emoji (4-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const emojiString = '🚀 🌍 🎉 👍 💻 🔥 ⚡ 🎯 🏆 💡 🌈 ✨ 🎨 🎭 🎪'; + byteLength = getUtf8Length(emojiString); + + // Write 1000 emoji strings + for (var i = 0; i < 1000; i++) { + writer.writeString(emojiString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed Unicode strings +/// +/// Real-world strings often contain a mix of ASCII, Latin Extended, +/// Cyrillic, CJK, and emoji characters. +class MixedUnicodeStringReadBenchmark extends BenchmarkBase { + MixedUnicodeStringReadBenchmark() : super('String read: mixed Unicode'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(); + const mixedString = 'Hello мир 世界 🌍! Test тест 测试 🚀'; + byteLength = getUtf8Length(mixedString); + + // Write 1000 mixed strings + for (var i = 0; i < 1000; i++) { + writer.writeString(mixedString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading VarString (length-prefixed strings) +class VarStringAsciiReadBenchmark extends BenchmarkBase { + VarStringAsciiReadBenchmark() : super('VarString read: ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + const asciiString = 'Hello, World! This is a test string.'; + + // Write 1000 VarStrings + for (var i = 0; i < 1000; i++) { + writer.writeVarString(asciiString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarString(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarString with mixed Unicode +class VarStringMixedReadBenchmark extends BenchmarkBase { + VarStringMixedReadBenchmark() : super('VarString read: mixed Unicode'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + const mixedString = 'Hello мир 世界 🌍 Test тест 测试 🚀'; + + // Write 1000 VarStrings + for (var i = 0; i < 1000; i++) { + writer.writeVarString(mixedString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarString(); + } + reader.reset(); + } +} + +/// Benchmark for reading empty strings +class EmptyStringReadBenchmark extends BenchmarkBase { + EmptyStringReadBenchmark() : super('String read: empty strings'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + + // Write 1000 empty strings + for (var i = 0; i < 1000; i++) { + writer.writeString(''); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(0); + } + reader.reset(); + } +} + +/// Benchmark for realistic message protocol with strings +/// +/// Simulates reading a typical JSON-like message structure with +/// multiple string fields of varying types and lengths. +class RealisticMessageReadBenchmark extends BenchmarkBase { + RealisticMessageReadBenchmark() : super('String read: realistic message'); + + late BinaryReader reader; + late Uint8List buffer; + late List fieldLengths; + + @override + void setup() { + final writer = BinaryWriter(); + + // Typical message fields + const fields = [ + 'user', // Field name (ASCII) + 'John Doe', // Value (ASCII) + 'email', // Field name (ASCII) + 'john.doe@example.com', // Value (ASCII) + 'message', // Field name (ASCII) + 'Hello 世界! 🌍', // Value (mixed Unicode) + 'timestamp', // Field name (ASCII) + '2024-12-30T12:00:00Z', // Value (ASCII) + 'locale', // Field name (ASCII) + 'ru-RU', // Value (ASCII) + ]; + + fieldLengths = fields.map(getUtf8Length).toList(); + + // Write 1000 messages + for (var i = 0; i < 1000; i++) { + fields.forEach(writer.writeString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + fieldLengths.forEach(reader.readString); + } + reader.reset(); + } +} + +/// Benchmark for alternating short and long strings +class AlternatingStringReadBenchmark extends BenchmarkBase { + AlternatingStringReadBenchmark() : super('String read: alternating lengths'); + + late BinaryReader reader; + late Uint8List buffer; + late int shortLength; + late int longLength; + + @override + void setup() { + final writer = BinaryWriter(); + const shortString = 'Hi'; + const longString = + 'This is a much longer string with more content to read and process'; + + shortLength = shortString.length; + longLength = longString.length; + + // Alternate between short and long strings + for (var i = 0; i < 1000; i++) { + writer + ..writeString(shortString) + ..writeString(longString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readString(shortLength) + ..readString(longLength); + } + reader.reset(); + } +} + +/// Benchmark for reading very long strings (> 1KB) +class VeryLongStringReadBenchmark extends BenchmarkBase { + VeryLongStringReadBenchmark() : super('String read: very long (>1KB)'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(); + // Create a ~2KB string + final longString = 'Lorem ipsum dolor sit amet. ' * 80; + stringLength = longString.length; + + // Write 1000 very long strings + for (var i = 0; i < 1000; i++) { + writer.writeString(longString); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +void main() { + test('ASCII string benchmarks:', () { + AsciiStringReadBenchmark().report(); + ShortAsciiStringReadBenchmark().report(); + LongAsciiStringReadBenchmark().report(); + EmptyStringReadBenchmark().report(); + }, tags: ['benchmark']); + + test('UTF-8 multi-byte benchmarks:', () { + CyrillicStringReadBenchmark().report(); + CjkStringReadBenchmark().report(); + EmojiStringReadBenchmark().report(); + MixedUnicodeStringReadBenchmark().report(); + }, tags: ['benchmark']); + + test('VarString benchmarks:', () { + VarStringAsciiReadBenchmark().report(); + VarStringMixedReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic string scenarios:', () { + RealisticMessageReadBenchmark().report(); + AlternatingStringReadBenchmark().report(); + VeryLongStringReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/reader/varint_read_bench_test.dart b/test/performance/reader/varint_read_bench_test.dart new file mode 100644 index 0000000..c5a4193 --- /dev/null +++ b/test/performance/reader/varint_read_bench_test.dart @@ -0,0 +1,348 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading VarUint in fast path (single byte: 0-127) +/// +/// This is the most common case in real-world protocols where small numbers +/// (lengths, counts, small IDs) dominate. The fast path should be highly +/// optimized as it's hit most frequently. +class VarUintFastPathBenchmark extends BenchmarkBase { + VarUintFastPathBenchmark() : super('VarUint read: 0-127 (fast path)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 single-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(i % 128); // Values 0-127 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 2-byte VarUint (128-16383) +/// +/// Second most common case - covers most typical array lengths, +/// message sizes, and medium-range IDs. +class VarUint2ByteBenchmark extends BenchmarkBase { + VarUint2ByteBenchmark() : super('VarUint read: 128-16383 (2 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 two-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(128 + (i % 100)); // Values 128-227 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 3-byte VarUint (16384-2097151) +class VarUint3ByteBenchmark extends BenchmarkBase { + VarUint3ByteBenchmark() : super('VarUint read: 16384-2097151 (3 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 three-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(16384 + (i % 1000) * 100); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 4-byte VarUint (2097152-268435455) +class VarUint4ByteBenchmark extends BenchmarkBase { + VarUint4ByteBenchmark() : super('VarUint read: 2097152-268435455 (4 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 four-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(2097152 + (i % 1000) * 10000); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 5-byte VarUint (268435456+) +/// +/// Less common in practice but important for large file sizes, +/// timestamps, or 64-bit IDs. +class VarUint5ByteBenchmark extends BenchmarkBase { + VarUint5ByteBenchmark() : super('VarUint read: 268435456+ (5 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 five-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(268435456 + i * 1000000); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading VarInt with ZigZag encoding (small positive values) +/// +/// ZigZag encoding: 0=>0, 1=>2, 2=>4, etc. +/// Tests decoding performance for positive signed integers. +class VarIntPositiveBenchmark extends BenchmarkBase { + VarIntPositiveBenchmark() : super('VarInt read: positive (ZigZag)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 positive VarInts + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i % 1000); // Values 0-999 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarInt(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarInt with ZigZag encoding (small negative values) +/// +/// ZigZag encoding: -1=>1, -2=>3, -3=>5, etc. +/// Tests decoding performance for negative signed integers. +class VarIntNegativeBenchmark extends BenchmarkBase { + VarIntNegativeBenchmark() : super('VarInt read: negative (ZigZag)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 negative VarInts + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(-(i % 1000 + 1)); // Values -1 to -1000 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarInt(); + } + + reader.reset(); + } +} + +/// Benchmark for reading mixed VarInt values (positive and negative) +/// +/// Realistic scenario where data contains both positive and negative values. +class VarIntMixedBenchmark extends BenchmarkBase { + VarIntMixedBenchmark() : super('VarInt read: mixed positive/negative'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 mixed VarInts + for (var i = 0; i < 1000; i++) { + final value = i.isEven ? (i ~/ 2) % 100 : -((i ~/ 2) % 100 + 1); + writer.writeVarInt(value); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarInt(); + } + + reader.reset(); + } +} + +/// Benchmark for reading mixed sizes VarUint (realistic distribution) +/// +/// Simulates real-world usage where most values are small (1-2 bytes) +/// but occasionally large values appear. +/// Distribution: 70% single-byte, 20% two-byte, 8% three-byte, 2% four-byte+ +class VarUintMixedSizesBenchmark extends BenchmarkBase { + VarUintMixedSizesBenchmark() : super('VarUint read: mixed sizes (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 VarUints with realistic distribution + for (var i = 0; i < 1000; i++) { + final mod = i % 100; + if (mod < 70) { + // 70% single byte + writer.writeVarUint(i % 128); + } else if (mod < 90) { + // 20% two bytes + writer.writeVarUint(128 + (i % 1000)); + } else if (mod < 98) { + // 8% three bytes + writer.writeVarUint(16384 + (i % 10000)); + } else { + // 2% four+ bytes + writer.writeVarUint(2097152 + i * 1000); + } + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarUint(); + } + + reader.reset(); + } +} + +void main() { + test('VarUint size benchmarks:', () { + VarUintFastPathBenchmark().report(); + VarUint2ByteBenchmark().report(); + VarUint3ByteBenchmark().report(); + VarUint4ByteBenchmark().report(); + VarUint5ByteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarInt (ZigZag) benchmarks:', () { + VarIntPositiveBenchmark().report(); + VarIntNegativeBenchmark().report(); + VarIntMixedBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios:', () { + VarUintMixedSizesBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/binary_write_bench_test.dart b/test/performance/writer/binary_write_bench_test.dart new file mode 100644 index 0000000..8f78fe2 --- /dev/null +++ b/test/performance/writer/binary_write_bench_test.dart @@ -0,0 +1,371 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing small byte arrays (< 16 bytes) +class SmallBytesWriteBenchmark extends BenchmarkBase { + SmallBytesWriteBenchmark() : super('Bytes write: small (8 bytes)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing medium byte arrays (64 bytes) +class MediumBytesWriteBenchmark extends BenchmarkBase { + MediumBytesWriteBenchmark() : super('Bytes write: medium (64 bytes)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(64, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing large byte arrays (1 KB) +class LargeBytesWriteBenchmark extends BenchmarkBase { + LargeBytesWriteBenchmark() : super('Bytes write: large (1 KB)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing very large byte arrays (64 KB) +class VeryLargeBytesWriteBenchmark extends BenchmarkBase { + VeryLargeBytesWriteBenchmark() : super('Bytes write: very large (64 KB)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes (length-prefixed byte arrays) +class VarBytesSmallWriteBenchmark extends BenchmarkBase { + VarBytesSmallWriteBenchmark() : super('VarBytes write: small'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes with medium-sized data +class VarBytesMediumWriteBenchmark extends BenchmarkBase { + VarBytesMediumWriteBenchmark() : super('VarBytes write: medium'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes with large data +class VarBytesLargeWriteBenchmark extends BenchmarkBase { + VarBytesLargeWriteBenchmark() : super('VarBytes write: large'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing empty byte arrays +class EmptyBytesWriteBenchmark extends BenchmarkBase { + EmptyBytesWriteBenchmark() : super('Bytes write: empty'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes([]); + } + writer.reset(); + } +} + +/// Benchmark for mixed-size byte writes (realistic scenario) +class MixedBytesWriteBenchmark extends BenchmarkBase { + MixedBytesWriteBenchmark() : super('Bytes write: mixed sizes (realistic)'); + + late BinaryWriter writer; + late Uint8List header; + late List payloads; + late Uint8List checksum; + + @override + void setup() { + writer = BinaryWriter(); + header = Uint8List.fromList(List.generate(16, (j) => j)); + payloads = [ + Uint8List.fromList(List.generate(64, (j) => j % 256)), + Uint8List.fromList(List.generate(128, (j) => j % 256)), + Uint8List.fromList(List.generate(256, (j) => j % 256)), + ]; + checksum = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer + ..writeBytes(header) + ..writeBytes(payloads[i % 3]) + ..writeBytes(checksum); + } + writer.reset(); + } +} + +/// Benchmark for alternating small and large writes +class AlternatingBytesWriteBenchmark extends BenchmarkBase { + AlternatingBytesWriteBenchmark() : super('Bytes write: alternating sizes'); + + late BinaryWriter writer; + late Uint8List small; + late Uint8List large; + + @override + void setup() { + writer = BinaryWriter(); + small = Uint8List.fromList([1, 2, 3, 4]); + large = Uint8List.fromList(List.generate(512, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer + ..writeBytes(small) + ..writeBytes(large); + } + writer.reset(); + } +} + +/// Benchmark for sequential small writes +class SequentialSmallWritesBenchmark extends BenchmarkBase { + SequentialSmallWritesBenchmark() + : super('Bytes write: sequential small writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + writer.reset(); + } +} + +/// Benchmark for writing bytes from List of int +class ListIntWriteBenchmark extends BenchmarkBase { + ListIntWriteBenchmark() : super('Bytes write: from List'); + + late BinaryWriter writer; + late List data; + + @override + void setup() { + writer = BinaryWriter(); + data = List.generate(64, (i) => i % 256); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing bytes from Uint8List view +class Uint8ListViewWriteBenchmark extends BenchmarkBase { + Uint8ListViewWriteBenchmark() : super('Bytes write: Uint8List view'); + + late BinaryWriter writer; + late Uint8List data; + late Uint8List view; + + @override + void setup() { + writer = BinaryWriter(); + data = Uint8List.fromList(List.generate(128, (i) => i % 256)); + view = Uint8List.view(data.buffer, 32, 64); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(view); + } + writer.reset(); + } +} + +void main() { + test('Fixed-size writes benchmarks:', () { + EmptyBytesWriteBenchmark().report(); + SmallBytesWriteBenchmark().report(); + MediumBytesWriteBenchmark().report(); + LargeBytesWriteBenchmark().report(); + VeryLargeBytesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarBytes (length-prefixed) benchmarks:', () { + VarBytesSmallWriteBenchmark().report(); + VarBytesMediumWriteBenchmark().report(); + VarBytesLargeWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios benchmarks:', () { + MixedBytesWriteBenchmark().report(); + AlternatingBytesWriteBenchmark().report(); + SequentialSmallWritesBenchmark().report(); + }, tags: ['benchmark']); + + test('Special input types benchmarks:', () { + ListIntWriteBenchmark().report(); + Uint8ListViewWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/buffer_growth_bench_test.dart b/test/performance/writer/buffer_growth_bench_test.dart new file mode 100644 index 0000000..9b791b7 --- /dev/null +++ b/test/performance/writer/buffer_growth_bench_test.dart @@ -0,0 +1,356 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for buffer growth from small initial size +class BufferGrowthSmallInitialBenchmark extends BenchmarkBase { + BufferGrowthSmallInitialBenchmark() + : super('Buffer growth: small initial (16 bytes -> 1KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16); + } + + @override + void exercise() => run(); + + @override + void run() { + // Write 1KB of data, forcing multiple expansions + for (var i = 0; i < 256; i++) { + writer.writeUint32(i); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth from medium initial size +class BufferGrowthMediumInitialBenchmark extends BenchmarkBase { + BufferGrowthMediumInitialBenchmark() + : super('Buffer growth: medium initial (256 bytes -> 64KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 256); + } + + @override + void exercise() => run(); + + @override + void run() { + // Write 64KB of data + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + for (var i = 0; i < 256; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with incremental writes +class BufferGrowthIncrementalBenchmark extends BenchmarkBase { + BufferGrowthIncrementalBenchmark() + : super('Buffer growth: incremental writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 64); + } + + @override + void exercise() => run(); + + @override + void run() { + // Write progressively larger chunks + for (var size = 1; size <= 256; size *= 2) { + final data = Uint8List.fromList(List.generate(size, (i) => i % 256)); + for (var i = 0; i < 10; i++) { + writer.writeBytes(data); + } + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with large single write +class BufferGrowthLargeSingleWriteBenchmark extends BenchmarkBase { + BufferGrowthLargeSingleWriteBenchmark() + : super('Buffer growth: large single write'); + + late BinaryWriter writer; + late Uint8List largeData; + + @override + void setup() { + writer = BinaryWriter(); + largeData = Uint8List.fromList(List.generate(32768, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + // Single large write that forces expansion + writer + ..writeBytes(largeData) + ..reset(); + } +} + +/// Benchmark for buffer growth with string writes +class BufferGrowthStringWritesBenchmark extends BenchmarkBase { + BufferGrowthStringWritesBenchmark() : super('Buffer growth: string writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32); + } + + @override + void exercise() => run(); + + @override + void run() { + const testString = 'Hello World! This is a test string.'; + for (var i = 0; i < 500; i++) { + writer.writeString(testString); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with VarInt writes +class BufferGrowthVarIntWritesBenchmark extends BenchmarkBase { + BufferGrowthVarIntWritesBenchmark() : super('Buffer growth: VarInt writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 1024); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer.writeVarUint(i & 0x7F); // Keep values to 0-127 (single byte) + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with mixed writes +class BufferGrowthMixedWritesBenchmark extends BenchmarkBase { + BufferGrowthMixedWritesBenchmark() : super('Buffer growth: mixed data types'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 64); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 200; i++) { + writer + ..writeUint8(i % 256) + ..writeUint32(i * 1000, .little) + ..writeFloat64(i * 3.14, .little) + ..writeString('Message $i') + ..writeVarUint(i); + } + writer.reset(); + } +} + +/// Benchmark for no buffer growth (sufficient initial size) +class NoBufferGrowthBenchmark extends BenchmarkBase { + NoBufferGrowthBenchmark() + : super('No buffer growth: sufficient initial size'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + // Write 32KB without triggering growth + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + for (var i = 0; i < 128; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with VarBytes +class BufferGrowthVarBytesBenchmark extends BenchmarkBase { + BufferGrowthVarBytesBenchmark() : super('Buffer growth: VarBytes writes'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16 * 1024); + data = Uint8List.fromList(List.generate(32, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth pattern: write, reset, write larger +class BufferGrowthResetPatternBenchmark extends BenchmarkBase { + BufferGrowthResetPatternBenchmark() + : super('Buffer growth: write-reset-write pattern'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(); + } + + @override + void exercise() => run(); + + @override + void run() { + // First write: small + for (var i = 0; i < 16; i++) { + writer.writeUint32(i); + } + writer.reset(); + + // Second write: medium (may reuse buffer) + for (var i = 0; i < 64; i++) { + writer.writeUint32(i); + } + writer.reset(); + + // Third write: large (may grow buffer) + for (var i = 0; i < 256; i++) { + writer.writeUint32(i); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with alternating sizes +class BufferGrowthAlternatingSizesBenchmark extends BenchmarkBase { + BufferGrowthAlternatingSizesBenchmark() + : super('Buffer growth: alternating write sizes'); + + late BinaryWriter writer; + late Uint8List smallData; + late Uint8List largeData; + + @override + void setup() { + writer = BinaryWriter(); + smallData = Uint8List.fromList(List.generate(8, (i) => i)); + largeData = Uint8List.fromList(List.generate(512, (i) => i % 256)); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 50; i++) { + writer + ..writeBytes(smallData) + ..writeBytes(largeData) + ..writeBytes(smallData); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth reaching max reusable capacity +class BufferGrowthMaxCapacityBenchmark extends BenchmarkBase { + BufferGrowthMaxCapacityBenchmark() + : super('Buffer growth: reaching max capacity (64KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 1024); + } + + @override + void exercise() => run(); + + @override + void run() { + // Write exactly 64KB to test max reusable capacity + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + for (var i = 0; i < 64; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +void main() { + test('Initial size variations:', () { + BufferGrowthSmallInitialBenchmark().report(); + BufferGrowthMediumInitialBenchmark().report(); + NoBufferGrowthBenchmark().report(); + }, tags: ['benchmark']); + + test('Growth patterns:', () { + BufferGrowthIncrementalBenchmark().report(); + BufferGrowthLargeSingleWriteBenchmark().report(); + BufferGrowthAlternatingSizesBenchmark().report(); + }, tags: ['benchmark']); + + test('Data type specific growth:', () { + BufferGrowthStringWritesBenchmark().report(); + BufferGrowthVarIntWritesBenchmark().report(); + BufferGrowthVarBytesBenchmark().report(); + BufferGrowthMixedWritesBenchmark().report(); + }, tags: ['benchmark']); + + test('Reset and capacity patterns:', () { + BufferGrowthResetPatternBenchmark().report(); + BufferGrowthMaxCapacityBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/fixed_int_write_bench_test.dart b/test/performance/writer/fixed_int_write_bench_test.dart new file mode 100644 index 0000000..c43156d --- /dev/null +++ b/test/performance/writer/fixed_int_write_bench_test.dart @@ -0,0 +1,388 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing Uint8 +class Uint8WriteBenchmark extends BenchmarkBase { + Uint8WriteBenchmark() : super('Uint8 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + writer.reset(); + } +} + +/// Benchmark for writing Int8 +class Int8WriteBenchmark extends BenchmarkBase { + Int8WriteBenchmark() : super('Int8 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt8((i % 256) - 128); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint16 big-endian +class Uint16BigEndianWriteBenchmark extends BenchmarkBase { + Uint16BigEndianWriteBenchmark() : super('Uint16 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint16(i % 65536); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint16 little-endian +class Uint16LittleEndianWriteBenchmark extends BenchmarkBase { + Uint16LittleEndianWriteBenchmark() : super('Uint16 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint16(i % 65536, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int16 big-endian +class Int16BigEndianWriteBenchmark extends BenchmarkBase { + Int16BigEndianWriteBenchmark() : super('Int16 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i % 65536) - 32768); + } + writer.reset(); + } +} + +/// Benchmark for writing Int16 little-endian +class Int16LittleEndianWriteBenchmark extends BenchmarkBase { + Int16LittleEndianWriteBenchmark() : super('Int16 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i % 65536) - 32768, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint32 big-endian +class Uint32BigEndianWriteBenchmark extends BenchmarkBase { + Uint32BigEndianWriteBenchmark() : super('Uint32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint32(i * 1000); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint32 little-endian +class Uint32LittleEndianWriteBenchmark extends BenchmarkBase { + Uint32LittleEndianWriteBenchmark() : super('Uint32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint32(i * 1000, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int32 big-endian +class Int32BigEndianWriteBenchmark extends BenchmarkBase { + Int32BigEndianWriteBenchmark() : super('Int32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt32(i * 1000 - 500000); + } + writer.reset(); + } +} + +/// Benchmark for writing Int32 little-endian +class Int32LittleEndianWriteBenchmark extends BenchmarkBase { + Int32LittleEndianWriteBenchmark() : super('Int32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt32(i * 1000 - 500000, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint64 big-endian +class Uint64BigEndianWriteBenchmark extends BenchmarkBase { + Uint64BigEndianWriteBenchmark() : super('Uint64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint64 little-endian +class Uint64LittleEndianWriteBenchmark extends BenchmarkBase { + Uint64LittleEndianWriteBenchmark() : super('Uint64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int64 big-endian +class Int64BigEndianWriteBenchmark extends BenchmarkBase { + Int64BigEndianWriteBenchmark() : super('Int64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt64(i * 1000000 - 500000000); + } + writer.reset(); + } +} + +/// Benchmark for writing Int64 little-endian +class Int64LittleEndianWriteBenchmark extends BenchmarkBase { + Int64LittleEndianWriteBenchmark() : super('Int64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt64(i * 1000000 - 500000000, .little); + } + writer.reset(); + } +} + +/// Benchmark for mixed fixed-int writes (realistic scenario) +class MixedFixedIntWriteBenchmark extends BenchmarkBase { + MixedFixedIntWriteBenchmark() : super('Mixed fixed-int write (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeUint8(i % 256) + ..writeUint16(i % 65536, .little) + ..writeUint32(i * 1000, .little) + ..writeInt32(i * 100 - 5000, .little) + ..writeUint64(i * 1000000, .little) + ..writeInt8((i % 256) - 128) + ..writeInt16((i % 32768) - 16384, .little) + ..writeInt64(i * 1000000, .little); + } + writer.reset(); + } +} + +void main() { + test('8-bit integer benchmarks:', () { + Uint8WriteBenchmark().report(); + Int8WriteBenchmark().report(); + }, tags: ['benchmark']); + + test('16-bit integer benchmarks:', () { + Uint16BigEndianWriteBenchmark().report(); + Uint16LittleEndianWriteBenchmark().report(); + Int16BigEndianWriteBenchmark().report(); + Int16LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('32-bit integer benchmarks:', () { + Uint32BigEndianWriteBenchmark().report(); + Uint32LittleEndianWriteBenchmark().report(); + Int32BigEndianWriteBenchmark().report(); + Int32LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('64-bit integer benchmarks:', () { + Uint64BigEndianWriteBenchmark().report(); + Uint64LittleEndianWriteBenchmark().report(); + Int64BigEndianWriteBenchmark().report(); + Int64LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed integer benchmarks:', () { + MixedFixedIntWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/float_write_bench_test.dart b/test/performance/writer/float_write_bench_test.dart new file mode 100644 index 0000000..4ed162e --- /dev/null +++ b/test/performance/writer/float_write_bench_test.dart @@ -0,0 +1,333 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing Float32 big-endian +class Float32BigEndianWriteBenchmark extends BenchmarkBase { + Float32BigEndianWriteBenchmark() : super('Float32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 3.14159) - 500.0); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 little-endian +class Float32LittleEndianWriteBenchmark extends BenchmarkBase { + Float32LittleEndianWriteBenchmark() : super('Float32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 3.14159) - 500.0, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 small values +class Float32SmallValuesWriteBenchmark extends BenchmarkBase { + Float32SmallValuesWriteBenchmark() : super('Float32 write (small values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i % 100) * 0.01, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 large values +class Float32LargeValuesWriteBenchmark extends BenchmarkBase { + Float32LargeValuesWriteBenchmark() : super('Float32 write (large values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 1000000.0) - 500000000.0, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 special values +class Float32SpecialValuesWriteBenchmark extends BenchmarkBase { + Float32SpecialValuesWriteBenchmark() + : super('Float32 write (special values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer + ..writeFloat32(0, .little) + ..writeFloat32(.nan, .little) + ..writeFloat32(.infinity, .little) + ..writeFloat32(.negativeInfinity, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 big-endian +class Float64BigEndianWriteBenchmark extends BenchmarkBase { + Float64BigEndianWriteBenchmark() : super('Float64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i * 2.718281828) - 1000.0); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 little-endian +class Float64LittleEndianWriteBenchmark extends BenchmarkBase { + Float64LittleEndianWriteBenchmark() : super('Float64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i * 2.718281828) - 1000.0, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 small values +class Float64SmallValuesWriteBenchmark extends BenchmarkBase { + Float64SmallValuesWriteBenchmark() : super('Float64 write (small values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i % 100) * 0.001, .little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 large values +class Float64LargeValuesWriteBenchmark extends BenchmarkBase { + Float64LargeValuesWriteBenchmark() : super('Float64 write (large values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64( + (i * 1000000000.0) - 500000000000.0, + .little, + ); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 special values +class Float64SpecialValuesWriteBenchmark extends BenchmarkBase { + Float64SpecialValuesWriteBenchmark() + : super('Float64 write (special values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer + ..writeFloat64(0, .little) + ..writeFloat64(.nan, .little) + ..writeFloat64(.infinity, .little) + ..writeFloat64(.negativeInfinity, .little); + } + writer.reset(); + } +} + +/// Benchmark for mixed float writes (realistic scenario) +class MixedFloatWriteBenchmark extends BenchmarkBase { + MixedFloatWriteBenchmark() : super('Mixed float write (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + // Position (3D coordinates) + ..writeFloat32(i * 10.0, .little) + ..writeFloat32(i * 20.0, .little) + ..writeFloat32(i * 30.0, .little) + // Rotation (quaternion) + ..writeFloat32(0, .little) + ..writeFloat32(0, .little) + ..writeFloat32(0, .little) + ..writeFloat32(1, .little) + // Timestamp + ..writeFloat64(i * 0.016, .little) + // Color (RGBA) + ..writeFloat32(1, .little) + ..writeFloat32(0.5, .little) + ..writeFloat32(0, .little) + ..writeFloat32(1, .little); + } + writer.reset(); + } +} + +/// Benchmark for alternating Float32/Float64 +class AlternatingFloatWriteBenchmark extends BenchmarkBase { + AlternatingFloatWriteBenchmark() : super('Alternating Float32/Float64 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 500; i++) { + writer + ..writeFloat32(i * 3.14, .little) + ..writeFloat64(i * 2.718, .little); + } + writer.reset(); + } +} + +void main() { + test('Float32 benchmarks:', () { + Float32BigEndianWriteBenchmark().report(); + Float32LittleEndianWriteBenchmark().report(); + Float32SmallValuesWriteBenchmark().report(); + Float32LargeValuesWriteBenchmark().report(); + Float32SpecialValuesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Float64 benchmarks:', () { + Float64BigEndianWriteBenchmark().report(); + Float64LittleEndianWriteBenchmark().report(); + Float64SmallValuesWriteBenchmark().report(); + Float64LargeValuesWriteBenchmark().report(); + Float64SpecialValuesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed float benchmarks:', () { + MixedFloatWriteBenchmark().report(); + AlternatingFloatWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/pool_bench_test.dart b/test/performance/writer/pool_bench_test.dart new file mode 100644 index 0000000..e56bd48 --- /dev/null +++ b/test/performance/writer/pool_bench_test.dart @@ -0,0 +1,332 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for acquiring writers from pool (empty pool) +/// +/// Tests the performance of getting a new writer from the pool. +class PoolAcquireNewBenchmark extends BenchmarkBase { + PoolAcquireNewBenchmark() : super('Pool: acquire new writer'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for acquiring reused writers from pool +/// +/// Tests the performance when writers are reused from the pool. +class PoolAcquireReusedBenchmark extends BenchmarkBase { + PoolAcquireReusedBenchmark() : super('Pool: acquire reused writer'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + // Pre-fill pool with released writers + for (var i = 0; i < 10; i++) { + final writer = BinaryWriterPool.acquire() + ..writeBytes(List.generate(100, (j) => j % 256)); + writers.add(writer); + } + writers.forEach(BinaryWriterPool.release); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for releasing writers to pool +/// +/// Tests the performance of returning writers to the pool. +class PoolReleaseBenchmark extends BenchmarkBase { + PoolReleaseBenchmark() : super('Pool: release writer'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + for (var i = 0; i < 100; i++) { + writers.add(BinaryWriterPool.acquire()); + } + } + + @override + void exercise() => run(); + + @override + void run() { + for (final writer in writers) { + writer.writeBytes(List.generate(50, (j) => j % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for acquire + write + release cycle +/// +/// Full cycle: get writer, use it, return it to pool. +class PoolFullCycleBenchmark extends BenchmarkBase { + PoolFullCycleBenchmark() + : super('Pool: full cycle (acquire + write + release)'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire() + ..writeUint32(i) + ..writeString('test message $i') + ..writeBytes(List.generate(32, (j) => (i + j) % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for heavy writer usage with pool +/// +/// Simulates typical protocol message serialization using pool. +class PoolHeavyUsageBenchmark extends BenchmarkBase { + PoolHeavyUsageBenchmark() : super('Pool: heavy usage (realistic)'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 50; i++) { + final writer = BinaryWriterPool.acquire() + // Simulate message header + ..writeUint32(i) // Message ID + ..writeVarUint(i % 1000) // Message length + // Write payload + ..writeString('Header: $i'); + for (var j = 0; j < 5; j++) { + writer.writeFloat64(i * 3.14 + j); + } + writer.writeBytes(List.generate(256, (k) => (i + k) % 256)); + // Return to pool + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for sequential acquire operations +/// +/// Tests pool performance under sequential load without much release. +class PoolSequentialAcquireBenchmark extends BenchmarkBase { + PoolSequentialAcquireBenchmark() : super('Pool: sequential acquire'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + final writers = []; + // Acquire up to pool max size + for (var i = 0; i < 32; i++) { + writers.add(BinaryWriterPool.acquire()); + } + // Release all + writers.forEach(BinaryWriterPool.release); + } +} + +/// Benchmark for pool statistics queries +/// +/// Tests the performance of checking pool statistics. +class PoolStatisticsBenchmark extends BenchmarkBase { + PoolStatisticsBenchmark() : super('Pool: query statistics'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + for (var i = 0; i < 10; i++) { + final w = BinaryWriterPool.acquire() + ..writeBytes(List.generate(100, (j) => j % 256)); + writers.add(w); + } + } + + @override + void exercise() => run(); + + @override + void run() { + // Query statistics multiple times + for (var i = 0; i < 1000; i++) { + // This should ideally be cheap - just reading counters + final stat = BinaryWriterPool.stats; + // Use the stat to prevent optimization away + if (stat.pooled > 0) { + // Just to use the value + } + } + } +} + +/// Benchmark for mixed operations on pool +/// +/// Realistic pattern: acquire, use, release in varying patterns. +class PoolMixedOperationsBenchmark extends BenchmarkBase { + PoolMixedOperationsBenchmark() : super('Pool: mixed operations'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + final batch1 = []; + // Acquire batch + for (var i = 0; i < 10; i++) { + batch1.add(BinaryWriterPool.acquire()); + } + // Use first batch + for (final w in batch1) { + w.writeVarUint(42); + } + // Acquire second batch while first still active + final batch2 = []; + for (var i = 0; i < 10; i++) { + batch2.add(BinaryWriterPool.acquire()); + } + // Release first batch + batch1.forEach(BinaryWriterPool.release); + // Continue using second batch + for (final w in batch2) { + w.writeFloat32(3.14); + } + // Release second batch + batch2.forEach(BinaryWriterPool.release); + } +} + +/// Benchmark for pool with buffer reuse +/// +/// Tests how well buffers are reused when writers are recycled. +class PoolBufferReuseBenchmark extends BenchmarkBase { + PoolBufferReuseBenchmark() : super('Pool: buffer reuse efficiency'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void exercise() => run(); + + @override + void run() { + // Use pool with varying write sizes + for (var cycle = 0; cycle < 20; cycle++) { + final writer = BinaryWriterPool.acquire(); + // Write varying amount of data + final size = 64 * (cycle % 10 + 1); // 64, 128, 192, ..., 640 + writer.writeBytes(List.generate(size, (i) => i % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for reset statistics +/// +/// Tests the cost of resetting pool statistics. +class PoolResetStatisticsBenchmark extends BenchmarkBase { + PoolResetStatisticsBenchmark() : super('Pool: reset statistics'); + + @override + void setup() { + BinaryWriterPool.clear(); + // Generate some statistics by using pool + for (var i = 0; i < 100; i++) { + final w = BinaryWriterPool.acquire()..writeUint32(i); + BinaryWriterPool.release(w); + } + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + BinaryWriterPool.clear(); + // Do some work + final w = BinaryWriterPool.acquire()..writeUint32(i); + BinaryWriterPool.release(w); + } + } +} + +void main() { + test('Pool acquire operations:', () { + PoolAcquireNewBenchmark().report(); + PoolAcquireReusedBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool release operations:', () { + PoolReleaseBenchmark().report(); + PoolFullCycleBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool usage patterns:', () { + PoolHeavyUsageBenchmark().report(); + PoolSequentialAcquireBenchmark().report(); + PoolMixedOperationsBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool efficiency:', () { + PoolBufferReuseBenchmark().report(); + PoolStatisticsBenchmark().report(); + PoolResetStatisticsBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/string_write_bench_test.dart b/test/performance/writer/string_write_bench_test.dart new file mode 100644 index 0000000..58e0783 --- /dev/null +++ b/test/performance/writer/string_write_bench_test.dart @@ -0,0 +1,360 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing ASCII strings (fast path) +class AsciiStringWriteBenchmark extends BenchmarkBase { + AsciiStringWriteBenchmark() : super('String write: ASCII only'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Hello, World! This is a test string 123456789'); + } + writer.reset(); + } +} + +/// Benchmark for writing short ASCII strings +class ShortAsciiStringWriteBenchmark extends BenchmarkBase { + ShortAsciiStringWriteBenchmark() : super('String write: short ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 125; i++) { + writer + ..writeString('Hi') + ..writeString('Test') + ..writeString('Hello') + ..writeString('OK') + ..writeString('Error') + ..writeString('Success') + ..writeString('123') + ..writeString('ABC'); + } + writer.reset(); + } +} + +/// Benchmark for writing long ASCII strings +class LongAsciiStringWriteBenchmark extends BenchmarkBase { + LongAsciiStringWriteBenchmark() : super('String write: long ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + const longString = + 'The quick brown fox jumps over the lazy dog. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'; + for (var i = 0; i < 100; i++) { + writer.writeString(longString); + } + writer.reset(); + } +} + +/// Benchmark for writing Cyrillic strings (2-byte UTF-8) +class CyrillicStringWriteBenchmark extends BenchmarkBase { + CyrillicStringWriteBenchmark() + : super('String write: Cyrillic (2-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Привет мир! Это тестовая строка на русском языке.'); + } + writer.reset(); + } +} + +/// Benchmark for writing CJK strings (3-byte UTF-8) +class CjkStringWriteBenchmark extends BenchmarkBase { + CjkStringWriteBenchmark() : super('String write: CJK (3-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('你好世界!这是一个测试字符串。日本語のテストも含まれています。'); + } + writer.reset(); + } +} + +/// Benchmark for writing emoji strings (4-byte UTF-8) +class EmojiStringWriteBenchmark extends BenchmarkBase { + EmojiStringWriteBenchmark() : super('String write: Emoji (4-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('🚀 🌍 🎉 👍 💻 🔥 ⚡ 🎯 🏆 💡 🌈 ✨ 🎨 🎭 🎪'); + } + writer.reset(); + } +} + +/// Benchmark for writing mixed Unicode strings +class MixedUnicodeStringWriteBenchmark extends BenchmarkBase { + MixedUnicodeStringWriteBenchmark() : super('String write: mixed Unicode'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Hello мир 世界 🌍! Test тест 测试 🚀'); + } + writer.reset(); + } +} + +/// Benchmark for writing VarString (length-prefixed strings) +class VarStringAsciiWriteBenchmark extends BenchmarkBase { + VarStringAsciiWriteBenchmark() : super('VarString write: ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarString('Hello, World! This is a test string.'); + } + writer.reset(); + } +} + +/// Benchmark for writing VarString with mixed Unicode +class VarStringMixedWriteBenchmark extends BenchmarkBase { + VarStringMixedWriteBenchmark() : super('VarString write: mixed Unicode'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarString('Hello мир 世界 🌍 Test тест 测试 🚀'); + } + writer.reset(); + } +} + +/// Benchmark for writing empty strings +class EmptyStringWriteBenchmark extends BenchmarkBase { + EmptyStringWriteBenchmark() : super('String write: empty strings'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeString(''); + } + writer.reset(); + } +} + +/// Benchmark for realistic message protocol with strings +class RealisticMessageWriteBenchmark extends BenchmarkBase { + RealisticMessageWriteBenchmark() : super('String write: realistic message'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeString('user') + ..writeString('John Doe') + ..writeString('email') + ..writeString('john.doe@example.com') + ..writeString('message') + ..writeString('Hello 世界! 🌍') + ..writeString('timestamp') + ..writeString('2024-12-30T12:00:00Z') + ..writeString('locale') + ..writeString('ru-RU'); + } + writer.reset(); + } +} + +/// Benchmark for alternating short and long strings +class AlternatingStringWriteBenchmark extends BenchmarkBase { + AlternatingStringWriteBenchmark() + : super('String write: alternating lengths'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + const shortString = 'Hi'; + const longString = + 'This is a much longer string with more content to write and process'; + for (var i = 0; i < 500; i++) { + writer + ..writeString(shortString) + ..writeString(longString); + } + writer.reset(); + } +} + +/// Benchmark for writing very long strings (> 1KB) +class VeryLongStringWriteBenchmark extends BenchmarkBase { + VeryLongStringWriteBenchmark() : super('String write: very long (>1KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void exercise() => run(); + + @override + void run() { + final longString = 'Lorem ipsum dolor sit amet. ' * 80; + for (var i = 0; i < 50; i++) { + writer.writeString(longString); + } + writer.reset(); + } +} + +void main() { + test('ASCII string benchmarks:', () { + AsciiStringWriteBenchmark().report(); + ShortAsciiStringWriteBenchmark().report(); + LongAsciiStringWriteBenchmark().report(); + EmptyStringWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('UTF-8 multi-byte benchmarks:', () { + CyrillicStringWriteBenchmark().report(); + CjkStringWriteBenchmark().report(); + EmojiStringWriteBenchmark().report(); + MixedUnicodeStringWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarString benchmarks:', () { + VarStringAsciiWriteBenchmark().report(); + VarStringMixedWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic string scenarios:', () { + RealisticMessageWriteBenchmark().report(); + AlternatingStringWriteBenchmark().report(); + VeryLongStringWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/varint_write_bench_test.dart b/test/performance/writer/varint_write_bench_test.dart new file mode 100644 index 0000000..709986d --- /dev/null +++ b/test/performance/writer/varint_write_bench_test.dart @@ -0,0 +1,242 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing VarUint in fast path (0-127) +class VarUintFastPathWriteBenchmark extends BenchmarkBase { + VarUintFastPathWriteBenchmark() : super('VarUint write: 0-127 (fast path)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(i % 128); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 2-byte values +class VarUint2ByteWriteBenchmark extends BenchmarkBase { + VarUint2ByteWriteBenchmark() : super('VarUint write: 128-16383 (2 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(128 + (i % 1000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 3-byte values +class VarUint3ByteWriteBenchmark extends BenchmarkBase { + VarUint3ByteWriteBenchmark() + : super('VarUint write: 16384-2097151 (3 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(16384 + (i % 10000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 4-byte values +class VarUint4ByteWriteBenchmark extends BenchmarkBase { + VarUint4ByteWriteBenchmark() + : super('VarUint write: 2097152-268435455 (4 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(2097152 + (i % 100000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 5-byte values +class VarUint5ByteWriteBenchmark extends BenchmarkBase { + VarUint5ByteWriteBenchmark() : super('VarUint write: 268435456+ (5 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(268435456 + i); + } + writer.reset(); + } +} + +/// Benchmark for writing positive VarInt (ZigZag encoded) +class VarIntPositiveWriteBenchmark extends BenchmarkBase { + VarIntPositiveWriteBenchmark() : super('VarInt write: positive (ZigZag)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i); + } + writer.reset(); + } +} + +/// Benchmark for writing negative VarInt (ZigZag encoded) +class VarIntNegativeWriteBenchmark extends BenchmarkBase { + VarIntNegativeWriteBenchmark() : super('VarInt write: negative (ZigZag)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(-(i + 1)); + } + writer.reset(); + } +} + +/// Benchmark for writing mixed positive/negative VarInt +class VarIntMixedWriteBenchmark extends BenchmarkBase { + VarIntMixedWriteBenchmark() : super('VarInt write: mixed positive/negative'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i.isEven ? i : -i); + } + writer.reset(); + } +} + +/// Benchmark for realistic VarUint distribution +class VarUintMixedSizesWriteBenchmark extends BenchmarkBase { + VarUintMixedSizesWriteBenchmark() + : super('VarUint write: mixed sizes (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final mod = i % 100; + if (mod < 70) { + writer.writeVarUint(i % 128); + } else if (mod < 90) { + writer.writeVarUint(128 + (i % 1000)); + } else if (mod < 98) { + writer.writeVarUint(16384 + (i % 10000)); + } else { + writer.writeVarUint(2097152 + i); + } + } + writer.reset(); + } +} + +void main() { + test('VarUint size benchmarks:', () { + VarUintFastPathWriteBenchmark().report(); + VarUint2ByteWriteBenchmark().report(); + VarUint3ByteWriteBenchmark().report(); + VarUint4ByteWriteBenchmark().report(); + VarUint5ByteWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarInt (ZigZag) benchmarks:', () { + VarIntPositiveWriteBenchmark().report(); + VarIntNegativeWriteBenchmark().report(); + VarIntMixedWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios:', () { + VarUintMixedSizesWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart new file mode 100644 index 0000000..8035658 --- /dev/null +++ b/test/unit/binary_reader_test.dart @@ -0,0 +1,2040 @@ +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)); + }); + + 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)); + }); + + 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)); + }); + + test('multiple resets in sequence', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); + final reader = BinaryReader(buffer) + ..readUint8() + ..reset() + ..reset() + ..reset(); + + expect(reader.offset, equals(0)); + expect(reader.availableBytes, equals(3)); + }); + + test('read after buffer exhaustion and reset', () { + final buffer = Uint8List.fromList([0x42, 0x43]); + final reader = BinaryReader(buffer); + + expect(reader.readUint8(), equals(0x42)); + expect(reader.readUint8(), equals(0x43)); + expect(reader.availableBytes, equals(0)); + + reader.reset(); + expect(reader.readUint8(), equals(0x42)); + }); + }); + + 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('multiple reset calls with intermediate reads', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + 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)); + }); + + 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)); + } + }); + }); + }); +} diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart new file mode 100644 index 0000000..07da45f --- /dev/null +++ b/test/unit/binary_writer_test.dart @@ -0,0 +1,2780 @@ +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.defaultBufferSize, 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('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)); + }); + }); +}