diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2a87bad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Description + +A clear description of the bug. + +## Steps to Reproduce + +1. +2. +3. + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. + +## Code Sample + +```dart +// Minimal code to reproduce the issue +``` + +## Environment + +- **pro_binary version**: [e.g., 2.1.0] +- **Dart SDK version**: [e.g., 3.10.0] +- **Platform**: [e.g., Windows/Linux/macOS/Web] + +## Additional Context + +Any other information about the problem. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d0217b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Question or Discussion + url: https://github.com/pro100andrey/pro_binary/discussions + about: Ask questions or discuss ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..29d5f41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,33 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Feature Description + +A clear description of the feature you'd like to see. + +## Use Case + +Describe the problem this feature would solve or the use case it addresses. + +## Proposed Solution + +How you envision this feature working. + +## Example API (Optional) + +```dart +// Example of how the API might look +``` + +## Alternatives Considered + +What alternative solutions or features have you considered? + +## Additional Context + +Any other context, screenshots, or examples. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b0632e0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Description + +Brief description of the changes. + +## Type of Change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Code refactoring + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] I have updated the CHANGELOG.md + +## Testing + +Describe the tests you ran and how to reproduce them. + +## Additional Notes + +Any additional information or context. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f79f82e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +name: Publish to pub.dev + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Install dependencies + run: dart pub get + + - name: Verify package + run: dart pub publish --dry-run + + - name: Publish package + uses: k-paxian/dart-package-publisher@v1.6 + with: + credentialJson: ${{ secrets.PUB_CREDENTIALS }} + flutter: false + skipTests: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ab8259c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + sdk: [stable, beta] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ matrix.sdk }} + + - name: Print Dart version + run: dart --version + + - name: Install dependencies + run: dart pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze code + run: dart analyze --fatal-infos + + - name: Run tests + run: dart test + + - 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 + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.sdk == 'stable' + uses: codecov/codecov-action@v4 + with: + file: ./coverage/lcov.info + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 3a83c2f..1a5dd23 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ doc/api/ .flutter-plugins .flutter-plugins-dependencies + +# Coverage +coverage/ +test/.test_coverage.dart +*.lcov diff --git a/CHANGELOG.md b/CHANGELOG.md index 808eaa7..44acd08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.1.0 + +- **feat**: Added detailed error messages with context (offset, available bytes) +- **feat**: Added `toBytes()` method in `BinaryWriter` (returns buffer without reset) +- **feat**: Added `reset()` method in `BinaryWriter` (resets without returning data) +- **feat**: Added `allowMalformed` parameter to `readString` in `BinaryReader` +- **improvement**: Increased performance of read/write operations +- **improvement**: Optimized internal buffer management in `BinaryWriter` +- **improvement**: Added validation for all boundary conditions +- **test**: Added new tests for boundary checks and new methods +- **docs**: Updated documentation with better examples and error handling + ## 2.0.0 - Update dependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..617c1fc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing to pro_binary + +Thank you for your interest in contributing! 🎉 + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/pro_binary.git` +3. Create a branch: `git checkout -b feature/my-feature` +4. Install dependencies: `dart pub get` + +## Development + +### Running Tests + +```bash +# Run all tests +dart test + +# Run specific test file +dart test test/binary_reader_test.dart + +# Run with coverage +dart pub global activate coverage +dart pub global run coverage:test_with_coverage +``` + +### Code Style + +```bash +# Format code +dart format . + +# Analyze code +dart analyze + +# Fix common issues +dart fix --apply +``` + +### Before Submitting + +- [ ] All tests pass (`dart test`) +- [ ] Code is formatted (`dart format .`) +- [ ] No analysis issues (`dart analyze`) +- [ ] Added tests for new features +- [ ] Updated CHANGELOG.md +- [ ] Updated documentation if needed + +## Pull Request Process + +1. Update the README.md with details of changes if applicable +2. Update the CHANGELOG.md with a note describing your changes +3. Ensure all tests pass and code is properly formatted +4. Submit a pull request with a clear description of changes + +## Reporting Bugs + +Use the [Bug Report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: + +- Clear description of the issue +- Steps to reproduce +- Expected vs actual behavior +- Code sample +- Environment details + +## Suggesting Features + +Use the [Feature Request template](.github/ISSUE_TEMPLATE/feature_request.md) and describe: + +- The feature you'd like +- Your use case +- Proposed API (if applicable) +- Alternative solutions considered + +## Code of Conduct + +- Be respectful and inclusive +- Provide constructive feedback +- Focus on what is best for the community + +## Questions? + +Feel free to open a [Discussion](https://github.com/pro100andrey/pro_binary/discussions) or reach out to maintainers. + +Thank you for contributing! 🚀 diff --git a/README.md b/README.md index 351c3ad..c5fed83 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ -# pro_binary - Binary Read/Write Library +# pro_binary -This library provides efficient binary reading and writing capabilities in Dart. It supports various data types and endianness, making it ideal for low-level data manipulation and network protocols. +[![pub package](https://img.shields.io/pub/v/pro_binary.svg)](https://pub.dev/packages/pro_binary) +[![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. ## Features -- Read and write operations for various data types (e.g., int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64). -- Support for both big-endian and little-endian formats. -- Efficient memory management with dynamic buffer resizing. +- ✅ 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 ## Installation @@ -19,79 +26,106 @@ dependencies: Then, run `pub get` to install the package. -## Usage +## Quick Start -### Writing Binary Data +### Writing -``` dart +```dart import 'package:pro_binary/pro_binary.dart'; void main() { 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) - ..writeBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255]) - ..writeString('Hello, World!'); + ..writeUint32(1000000, Endian.little) + ..writeFloat64(3.14159) + ..writeString('Hello'); final bytes = writer.takeBytes(); - print(bytes); + print('Written ${bytes.length} bytes'); } ``` -### Reading Binary Data +### Reading -``` dart +```dart +import 'dart:typed_data'; import 'package:pro_binary/pro_binary.dart'; void main() { - final buffer = Uint8List.fromList([ - 42, 214, 255, 255, 0, 128, 255, 255, 255, 255, 0, 0, 0, 128, - 255, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 128, - 195, 245, 72, 64, 24, 45, 68, 84, 251, 33, 9, 64, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, 72, - 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33 - ]); - - final reader = BinaryReader(buffer); - - final uint8 = reader.readUint8(); - final int8 = reader.readInt8(); - final uint16 = reader.readUint16(Endian.little); - final int16 = reader.readInt16(Endian.little); - final uint32 = reader.readUint32(Endian.little); - final int32 = reader.readInt32(Endian.little); - final uint64 = reader.readUint64(Endian.little); - final int64 = reader.readInt64(Endian.little); - final float32 = reader.readFloat32(Endian.little); - final float64 = reader.readFloat64(Endian.little); - final bytes = reader.readBytes(13); - final string = reader.readString(13); - - print([uint8, int8, uint16, int16, uint32, int32, uint64, int64, float32, float64, bytes, string]); + final data = Uint8List.fromList([42, 64, 66, 15, 0]); + final reader = BinaryReader(data); + + final value1 = reader.readUint8(); // 42 + final value2 = reader.readUint32(Endian.little); // 1000000 + + print('Read: $value1, $value2'); + print('Remaining: ${reader.availableBytes} bytes'); } ``` -## Running Tests +## API Overview + +### BinaryWriter + +```dart +final writer = BinaryWriter(initialBufferSize: 64); + +// Write operations +writer.writeUint8(255); +writer.writeInt32(-1000, Endian.big); +writer.writeFloat64(3.14); +writer.writeBytes([1, 2, 3]); +writer.writeString('text'); + +// Buffer operations +final bytes = writer.toBytes(); // Get copy without reset +final result = writer.takeBytes(); // Get and reset +writer.clear(); // Reset without returning +print(writer.bytesWritten); // Check written size +``` + +### BinaryReader -To run the tests, use the following command: +```dart +final reader = BinaryReader(buffer); -``` bash -dart test +// Read operations +final u8 = reader.readUint8(); +final i32 = reader.readInt32(Endian.little); +final f64 = reader.readFloat64(); +final bytes = reader.readBytes(10); +final text = reader.readString(5); + +// Navigation +reader.skip(4); // Skip bytes +final pos = reader.offset; // Current position +reader.reset(); // Reset to start +print(reader.availableBytes); // Remaining bytes ``` -This will execute all tests in the `test` directory and provide a summary of the results. +## Error Handling + +All read operations validate boundaries and provide detailed error messages: + +```dart +try { + reader.readUint32(); // Not enough bytes +} catch (e) { + // RangeError: Not enough bytes to read Uint32: + // required 4 bytes, available 2 bytes at offset 10 +} +``` ## Contributing -Feel free to open [issues](https://github.com/pro100andrey/pro_binary/issues) or submit [pull requests](https://github.com/pro100andrey/pro_binary/pulls) on GitHub. Contributions are always welcome! +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on: + +- How to set up the development environment +- Running tests and coverage +- Code style and formatting +- Submitting pull requests + +For bugs and features, use the [issue templates](https://github.com/pro100andrey/pro_binary/issues/new/choose). ## License diff --git a/analysis_options.yaml b/analysis_options.yaml index 5860f2c..a51e948 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,6 @@ include: package:pro_lints/common.yaml +formatter: + trailing_commas: preserve + diff --git a/example/main.dart b/example/main.dart index 89b3cf4..66e5e06 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,67 +1,75 @@ +// ignore_for_file: avoid_print + import 'dart:typed_data'; import 'package:pro_binary/pro_binary.dart'; -void main(List args) { - // ignore: avoid_print - print('BinaryWriter\n'); +void main() { + writeExample(); + readExample(); + errorHandlingExample(); + bufferManagementExample(); +} + +void writeExample() { + print('=== Writing Binary Data ==='); + 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) - ..writeBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255]) + ..writeInt32(-1000, Endian.little) + ..writeFloat64(3.14159) ..writeString('Hello, World!'); final bytes = writer.takeBytes(); - // ignore: avoid_print - print(bytes); + print('Written ${bytes.length} bytes: $bytes\n'); +} - // ignore: avoid_print - print('BinaryReader\n'); +void readExample() { + print('=== Reading Binary Data ==='); final buffer = Uint8List.fromList([ - 42, 214, 255, 255, 0, 128, 255, 255, 255, 255, 0, 0, 0, 128, // - 255, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 128, // - 195, 245, 72, 64, 24, 45, 68, 84, 251, 33, 9, 64, // - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, // - 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, // + 42, 24, 252, 255, 255, // uint8 + int32 + 31, 133, 235, 81, 184, 30, 9, 64, // float64 + 72, 101, 108, 108, 111, // "Hello" ]); final reader = BinaryReader(buffer); - final uint8 = reader.readUint8(); - final int8 = reader.readInt8(); - final uint16 = reader.readUint16(Endian.little); - final int16 = reader.readInt16(Endian.little); - final uint32 = reader.readUint32(Endian.little); - final int32 = reader.readInt32(Endian.little); - final uint64 = reader.readUint64(Endian.little); - final int64 = reader.readInt64(Endian.little); - final float32 = reader.readFloat32(Endian.little); - final float64 = reader.readFloat64(Endian.little); - final bytesData = reader.readBytes(13); - final string = reader.readString(13); - - // ignore: avoid_print - print([ - uint8, - int8, - uint16, - int16, - uint32, - int32, - uint64, - int64, - float32, - float64, - bytesData, - string, - ]); + print('uint8: ${reader.readUint8()}'); + print('int32: ${reader.readInt32(Endian.little)}'); + print('float64: ${reader.readFloat64()}'); + print('string: ${reader.readString(5)}'); + print('Position: ${reader.offset}/${buffer.length}\n'); +} + +void errorHandlingExample() { + print('=== Error Handling ==='); + + final buffer = Uint8List(2); // Only 2 bytes + final reader = BinaryReader(buffer); + + try { + reader.readUint32(); // Needs 4 bytes + } on Object catch (e) { + print('Caught: $e\n'); + } +} + +void bufferManagementExample() { + print('=== Buffer Management ==='); + + final writer = BinaryWriter() + ..writeUint8(1) + ..writeUint8(2); + + // Inspect without consuming + print('Current buffer: ${writer.toBytes()}'); + + writer.writeUint8(3); + print('After adding: ${writer.toBytes()}'); + + // Take and reset + final result = writer.takeBytes(); + print('Final result: $result'); + print('After takeBytes: ${writer.toBytes()}'); } diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index fa20d29..b8d9c71 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -3,18 +3,63 @@ import 'dart:typed_data'; import 'binary_reader_interface.dart'; -/// The [BinaryReader] class is an implementation of the [BinaryReaderInterface] -/// used to decode various types of data from a binary +/// A high-performance implementation of [BinaryReaderInterface] for decoding +/// binary data. +/// +/// Features: +/// - Zero-copy operations using ByteData views +/// - Inline bounds checking for safety +/// - Support for big-endian and little-endian byte order +/// - UTF-8 string decoding +/// - Peek operations without advancing position +/// +/// Memory Management: +/// - Uses views (zero-copy) for [peekBytes] and [readBytes] +/// - [readString] decodes directly from the buffer view +/// - No internal allocations except for decoded strings +/// +/// Example: +/// ```dart +/// final bytes = Uint8List.fromList([0, 0, 0, 42]); +/// final reader = BinaryReader(bytes); +/// final value = reader.readUint32(); // 42 +/// print(reader.availableBytes); // 0 +/// ``` class BinaryReader extends BinaryReaderInterface { - BinaryReader(this._buffer) - : _data = ByteData.sublistView(_buffer), - _length = _buffer.length; - + /// Creates a new [BinaryReader] for 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 underlying byte buffer being read from. final Uint8List _buffer; + + /// Efficient view for typed data access. final ByteData _data; + + /// Total length of the buffer. final int _length; + + /// Current read position in the buffer. int _offset = 0; + /// Performs inline bounds check to ensure safe reads. + /// + /// Throws [AssertionError] if attempting to read beyond buffer boundaries. + @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', + ); + } + @override int get availableBytes => _length - _offset; @@ -25,26 +70,25 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readUint8() { - final value = _data.getUint8(_offset); - _offset += 1; - - return value; + _checkBounds(1, 'Uint8'); + return _data.getUint8(_offset++); } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override int readInt8() { - final value = _data.getInt8(_offset); - _offset += 1; + _checkBounds(1, 'Int8'); - return value; + return _data.getInt8(_offset++); } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override int readUint16([Endian endian = Endian.big]) { + _checkBounds(2, 'Uint16'); + final value = _data.getUint16(_offset, endian); _offset += 2; @@ -55,6 +99,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readInt16([Endian endian = Endian.big]) { + _checkBounds(2, 'Int16'); + final value = _data.getInt16(_offset, endian); _offset += 2; @@ -65,6 +111,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readUint32([Endian endian = Endian.big]) { + _checkBounds(4, 'Uint32'); + final value = _data.getUint32(_offset, endian); _offset += 4; @@ -75,6 +123,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readInt32([Endian endian = Endian.big]) { + _checkBounds(4, 'Int32'); + final value = _data.getInt32(_offset, endian); _offset += 4; @@ -85,6 +135,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readUint64([Endian endian = Endian.big]) { + _checkBounds(8, 'Uint64'); + final value = _data.getUint64(_offset, endian); _offset += 8; @@ -95,6 +147,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override int readInt64([Endian endian = Endian.big]) { + _checkBounds(8, 'Int64'); + final value = _data.getInt64(_offset, endian); _offset += 8; @@ -105,6 +159,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override double readFloat32([Endian endian = Endian.big]) { + _checkBounds(4, 'Float32'); + final value = _data.getFloat32(_offset, endian); _offset += 4; @@ -115,6 +171,8 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override double readFloat64([Endian endian = Endian.big]) { + _checkBounds(8, 'Float64'); + final value = _data.getFloat64(_offset, endian); _offset += 8; @@ -125,8 +183,10 @@ class BinaryReader extends BinaryReaderInterface { @pragma('dart2js:tryInline') @override Uint8List readBytes(int length) { - final bytes = Uint8List.sublistView(_buffer, _offset, _offset + length); + assert(length >= 0, 'Length must be non-negative'); + _checkBounds(length, 'Bytes'); + final bytes = Uint8List.sublistView(_buffer, _offset, _offset + length); _offset += length; return bytes; @@ -135,55 +195,39 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - String readString(int length) { - final bytes = readBytes(length); + String readString(int length, {bool allowMalformed = false}) { + if (length == 0) { + return ''; + } + + _checkBounds(length, 'String'); - return utf8.decode(bytes); + final view = Uint8List.sublistView(_buffer, _offset, _offset + length); + _offset += length; + + return utf8.decode(view, allowMalformed: allowMalformed); } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override Uint8List peekBytes(int length, [int? offset]) { - if (length == 0) { - throw ArgumentError.value(length, 'Length must be greater than zero.'); - } + assert(length >= 0, 'Length must be non-negative'); - if (offset != null && offset < 0) { - throw ArgumentError.value( - offset, - 'Offset must be greater than or equal to zero.', - ); + if (length == 0) { + return Uint8List(0); } final peekOffset = offset ?? _offset; + _checkBounds(length, 'Peek Bytes', peekOffset); - if (peekOffset < 0) { - throw ArgumentError.value( - peekOffset, - 'Offset must be greater than or equal to zero.', - ); - } - - return _data.buffer.asUint8List(peekOffset, length); + return Uint8List.sublistView(_buffer, peekOffset, peekOffset + length); } @override void skip(int length) { - if (length < 0) { - throw ArgumentError.value( - length, - 'Length must be greater than or equal to zero.', - ); - } - - if (_offset + length > _length) { - throw ArgumentError.value( - length, - 'Offset is out of bounds.', - ); - } - + assert(length >= 0, 'Length must be non-negative'); + _checkBounds(length, 'Skip'); _offset += length; } @@ -193,4 +237,7 @@ class BinaryReader extends BinaryReaderInterface { void reset() { _offset = 0; } + + @override + int get offset => _offset; } diff --git a/lib/src/binary_reader_interface.dart b/lib/src/binary_reader_interface.dart index 57ca1d6..fbb77cb 100644 --- a/lib/src/binary_reader_interface.dart +++ b/lib/src/binary_reader_interface.dart @@ -201,11 +201,14 @@ abstract class BinaryReaderInterface { /// 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); + String readString(int length, {bool allowMalformed = false}); /// Peeks a list of bytes from the buffer without changing the internal state. /// @@ -239,5 +242,27 @@ abstract class BinaryReaderInterface { 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 647fc35..15ff8de 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,22 +1,60 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'binary_writer_interface.dart'; -/// The [BinaryWriter] class is an implementation of the [BinaryWriterInterface] -/// used to encode various types of data into a binary format. +/// A high-performance implementation of [BinaryWriterInterface] for encoding +/// data into binary format. +/// +/// Features: +/// - Automatic buffer growth with 1.5x expansion strategy +/// - Cached capacity checks for minimal overhead +/// - Optimized for sequential writes +/// - Custom UTF-8 string encoding for performance +/// - Instance-level temporary buffers (thread-safe) +/// +/// Buffer Management: +/// - Automatically grows using 1.5x expansion strategy when capacity exceeded +/// - If required size exceeds 1.5x, grows to exact required size +/// - [takeBytes] returns a view (zero-copy) and resets the writer +/// - [toBytes] returns a view without resetting, allowing continued writing +/// - [reset] clears the buffer and reinitializes to initial size +/// +/// Thread Safety: +/// - Each writer instance is thread-safe within its own execution context +/// - Uses instance-level temporary buffers for float conversions +/// - Safe to use multiple writers concurrently in different isolates +/// +/// Example: +/// ```dart +/// final writer = BinaryWriter(); +/// 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 +/// ``` class BinaryWriter extends BinaryWriterInterface { + /// Creates a new [BinaryWriter] with an optional 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 { + : _initialBufferSize = initialBufferSize { _initializeBuffer(initialBufferSize); } final int _initialBufferSize; + /// Internal buffer for storing binary data. late Uint8List _buffer; - late ByteData _data; + + /// Current write position in the buffer. int _offset = 0; + /// Cached buffer capacity to avoid repeated length checks. + int _capacity = 0; + @override int get bytesWritten => _offset; @@ -24,101 +62,200 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('dart2js:tryInline') @override void writeUint8(int value) { - if (value < 0 || value > 255) { - throw RangeError.range(value, 0, 255, 'value'); - } + assert( + value >= 0 && value <= 255, + 'Value out of range for Uint8: $value', + ); _ensureSize(1); - _data.setUint8(_offset, value); - _offset += 1; + _buffer[_offset++] = value; } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeInt8(int value) { - if (value < -128 || value > 127) { - throw RangeError.range(value, -128, 127, 'value'); - } + assert( + value >= -128 && value <= 127, + 'Value out of range for Int8: $value', + ); _ensureSize(1); - _data.setInt8(_offset, value); - _offset += 1; + _buffer[_offset++] = value & 0xFF; } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeUint16(int value, [Endian endian = Endian.big]) { - if (value < 0 || value > 65535) { - throw RangeError.range(value, 0, 65535, 'value'); - } + assert( + value >= 0 && value <= 65535, + 'Value out of range for Uint16: $value', + ); _ensureSize(2); - _data.setUint16(_offset, value, endian); - _offset += 2; + + if (endian == Endian.big) { + _buffer[_offset++] = (value >> 8) & 0xFF; + _buffer[_offset++] = value & 0xFF; + } else { + _buffer[_offset++] = value & 0xFF; + _buffer[_offset++] = (value >> 8) & 0xFF; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeInt16(int value, [Endian endian = Endian.big]) { - if (value < -32768 || value > 32767) { - throw RangeError.range(value, -32768, 32767, 'value'); - } + assert( + value >= -32768 && value <= 32767, + 'Value out of range for Int16: $value', + ); _ensureSize(2); - _data.setInt16(_offset, value, endian); - _offset += 2; + + if (endian == Endian.big) { + _buffer[_offset++] = (value >> 8) & 0xFF; + _buffer[_offset++] = value & 0xFF; + } else { + _buffer[_offset++] = value & 0xFF; + _buffer[_offset++] = (value >> 8) & 0xFF; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeUint32(int value, [Endian endian = Endian.big]) { - if (value < 0 || value > 4294967295) { - throw RangeError.range(value, 0, 4294967295, 'value'); - } + assert( + value >= 0 && value <= 4294967295, + 'Value out of range for Uint32: $value', + ); _ensureSize(4); - _data.setUint32(_offset, value, endian); - _offset += 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; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeInt32(int value, [Endian endian = Endian.big]) { + assert( + value >= -2147483648 && value <= 2147483647, + 'Value out of range for Int32: $value', + ); + _ensureSize(4); - _data.setInt32(_offset, value, endian); - _offset += 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; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeUint64(int value, [Endian endian = Endian.big]) { + assert( + value >= 0 && value <= 9223372036854775807, + 'Value out of range for Uint64: $value', + ); + _ensureSize(8); - _data.setUint64(_offset, value, endian); - _offset += 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; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeInt64(int value, [Endian endian = Endian.big]) { + assert( + value >= -9223372036854775808 && value <= 9223372036854775807, + 'Value out of range for Int64: $value', + ); + _ensureSize(8); - _data.setInt64(_offset, value, endian); - _offset += 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; + } } + // Instance-level temporary buffers for float conversion (thread-safe) + final Uint8List _tempU8 = Uint8List(8); + late final Float32List _tempF32 = Float32List.view(_tempU8.buffer); + late final Float64List _tempF64 = Float64List.view(_tempU8.buffer); + @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeFloat32(double value, [Endian endian = Endian.big]) { _ensureSize(4); - _data.setFloat32(_offset, value, endian); - _offset += 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; + } } @pragma('vm:prefer-inline') @@ -126,34 +263,107 @@ class BinaryWriter extends BinaryWriterInterface { @override void writeFloat64(double value, [Endian endian = Endian.big]) { _ensureSize(8); - _data.setFloat64(_offset, value, endian); - _offset += 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; + } } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override void writeBytes(List bytes) { + // Early return for empty byte lists + if (bytes.isEmpty) { + return; + } + final length = bytes.length; _ensureSize(length); - final list = bytes is Uint8List ? bytes : Uint8List.fromList(bytes); - - _buffer.setRange(_offset, _offset + length, list); + _buffer.setRange(_offset, _offset + length, bytes); _offset += length; } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeString(String value) { - final length = value.length; - _ensureSize(length); + void writeString(String value, {bool allowMalformed = true}) { + final len = value.length; + if (len == 0) { + return; + } - final encoded = utf8.encode(value); + // Over-allocate max UTF-8 size (4 bytes/char) + _ensureSize(len * 4); + + var bufIdx = _offset; + for (var i = 0; i < len; i++) { + 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; + } + } + // Lone high surrogate + if (!allowMalformed) { + throw FormatException( + 'Invalid UTF-16: lone high surrogate at index $i', + value, + i, + ); + } + // 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, + ); + } + // 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); + } + } - _buffer.setRange(_offset, _offset + encoded.length, encoded); - _offset += encoded.length; + _offset = bufIdx; } @override @@ -166,26 +376,44 @@ class BinaryWriter extends BinaryWriterInterface { return result; } + @override + Uint8List toBytes() => Uint8List.sublistView(_buffer, 0, _offset); + + @override + void reset() { + _offset = 0; + _initializeBuffer(_initialBufferSize); + } + /// Initializes the buffer with the specified size. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void _initializeBuffer(int size) { _buffer = Uint8List(size); - _data = ByteData.view(_buffer.buffer); + _capacity = size; } /// Ensures that the buffer has enough space to accommodate the specified - /// size. If the buffer is too small, it expands it to the next power of two. + /// [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 requiredSize = _offset + size; - if (_buffer.length < requiredSize) { - final newSize = 1 << (requiredSize - 1).bitLength; - final newBuffer = Uint8List(newSize)..setRange(0, _offset, _buffer); + final req = _offset + size; + if (req <= _capacity) { + return; + } - _buffer = newBuffer; - _data = ByteData.view(_buffer.buffer); + var newCapacity = _capacity * 3 ~/ 2; // 1.5x + if (newCapacity < req) { + newCapacity = req; } + + final newBuffer = Uint8List(newCapacity)..setRange(0, _offset, _buffer); + _buffer = newBuffer; + _capacity = newCapacity; } } diff --git a/lib/src/binary_writer_interface.dart b/lib/src/binary_writer_interface.dart index 3661a67..024320a 100644 --- a/lib/src/binary_writer_interface.dart +++ b/lib/src/binary_writer_interface.dart @@ -118,8 +118,8 @@ abstract class BinaryWriterInterface { /// /// 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 scratch offset position with the - /// specified byte order (endian), and the scratch offset is incremented by 8 + /// 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 @@ -138,8 +138,8 @@ abstract class BinaryWriterInterface { /// /// 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 scratch offset position with the - /// specified byte order (endian), and the scratch offset is incremented by 8 + /// 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 @@ -158,8 +158,8 @@ abstract class BinaryWriterInterface { /// /// 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 scratch offset position with the specified byte - /// order (endian), and the scratch offset is incremented by 4 bytes. + /// 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 @@ -176,8 +176,8 @@ abstract class BinaryWriterInterface { /// /// 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 scratch offset position with the specified byte - /// order (endian), and the scratch offset is incremented by 8 bytes. + /// 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 @@ -220,20 +220,71 @@ abstract class BinaryWriterInterface { /// 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); + void writeString(String value, {bool allowMalformed = true}); - /// Returns the written bytes as a [Uint8List]. + /// 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. /// - /// If the builder is empty, it returns the current scratch buffer contents - /// as a [Uint8List] view. Otherwise, it appends the scratch buffer to the - /// builder - /// and returns the builder's bytes. + /// Use this method when you want to retrieve the data and start fresh. /// - /// This method also resets the internal state, preparing the writer for new - /// data. + /// 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/pubspec.yaml b/pubspec.yaml index d3b8581..2f76e21 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.0.0 +version: 2.1.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues @@ -22,10 +22,10 @@ topics: - deserialization environment: - sdk: ^3.6.0 + sdk: ^3.10.0 dev_dependencies: - benchmark_harness: ^2.3.1 - pro_lints: ^3.0.1 - test: ^1.25.14 + benchmark_harness: ^2.4.0 + pro_lints: ^5.0.0 + test: ^1.28.0 diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index 7324ab3..cc59edc 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -4,58 +4,74 @@ import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; class BinaryReaderBenchmark extends BenchmarkBase { - BinaryReaderBenchmark(this.iterations) - : super('BinaryReader performance test'); - - final int iterations; - - // Buffer with test data - final buffer = Uint8List.fromList([ - 42, // Uint8 - 214, // Int8 (two's complement of -42 is 214) - 255, 255, // Uint16 (65535 in little-endian) - 0, 128, // Int16 (-32768 in little-endian) - 255, 255, 255, 255, // Uint32 (4294967295 in little-endian) - 0, 0, 0, 128, // Int32 (-2147483648 in little-endian) - 255, 255, 255, 255, 255, 255, 255, - 127, // Uint64 (9223372036854775807 in little-endian) - 0, 0, 0, 0, 0, 0, 0, 128, // Int64 (-9223372036854775808 in little-endian) - 195, 245, 72, 64, // Float32 (3.14 in IEEE 754 format, little-endian) - 24, 45, 68, 84, 251, 33, 9, - 64, // Float64 (3.141592653589793 in IEEE 754 format, little-endian) - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, // Bytes - 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, - 33, - ]); + 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 < iterations; i++) { - reader - ..reset() - ..readUint8() - ..readInt8() - ..readUint16(Endian.little) - ..readInt16(Endian.little) - ..readUint32(Endian.little) - ..readInt32(Endian.little) - ..readUint64(Endian.little) - ..readInt64(Endian.little) - ..readFloat32(Endian.little) - ..readFloat64(Endian.little) - ..readBytes(13) - ..readString(13); // length of the byte array + 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(1000).report(); + BinaryReaderBenchmark.main(); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 19577db..a122d81 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -87,8 +87,16 @@ void main() { }); test('readUint64 big-endian', () { - final buffer = - Uint8List.fromList([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]); + final buffer = Uint8List.fromList([ + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + ]); final reader = BinaryReader(buffer); expect(reader.readUint64(), equals(4294967296)); @@ -96,8 +104,16 @@ void main() { }); test('readUint64 little-endian', () { - final buffer = - Uint8List.fromList([0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]); + final buffer = Uint8List.fromList([ + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + ]); final reader = BinaryReader(buffer); expect(reader.readUint64(Endian.little), equals(4294967296)); @@ -105,8 +121,16 @@ void main() { }); test('readInt64 big-endian', () { - final buffer = - Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + final buffer = Uint8List.fromList([ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ]); final reader = BinaryReader(buffer); expect(reader.readInt64(), equals(-1)); @@ -114,8 +138,16 @@ void main() { }); test('readInt64 little-endian', () { - final buffer = - Uint8List.fromList([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]); + final buffer = Uint8List.fromList([ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x80, + ]); final reader = BinaryReader(buffer); expect(reader.readInt64(Endian.little), equals(-9223372036854775808)); @@ -229,20 +261,22 @@ void main() { 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); + 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)); + 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)); - }); + 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]); @@ -259,81 +293,83 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('read beyond buffer throws RangeError', () { + test('read beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(reader.readUint32, throwsRangeError); + expect(reader.readUint32, throwsA(isA())); }); - test('negative length input throws ArgumentError', () { + test('negative length input throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(-1), throwsArgumentError); - expect(() => reader.skip(-5), throwsArgumentError); - expect(() => reader.peekBytes(-2), throwsArgumentError); + 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, throwsRangeError); + 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, throwsRangeError); + expect(reader.readUint8, throwsA(isA())); }); - test('peekBytes beyond buffer throws RangeError', () { + test('peekBytes beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.peekBytes(3), throwsRangeError); - expect(() => reader.peekBytes(1, 2), throwsRangeError); + expect(() => reader.peekBytes(3), throwsA(isA())); + expect(() => reader.peekBytes(1, 2), throwsA(isA())); }); - test('readString with insufficient bytes throws RangeError', () { + test('readString with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' final reader = BinaryReader(buffer); - expect(() => reader.readString(5), throwsRangeError); + expect(() => reader.readString(5), throwsA(isA())); }); - test('readBytes with insufficient bytes throws RangeError', () { + test('readBytes with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(3), throwsRangeError); + expect(() => reader.readBytes(3), throwsA(isA())); }); - test('read methods throw RangeError when not enough bytes', () { + test('read methods throw AssertionError when not enough bytes', () { final buffer = Uint8List.fromList([0x00, 0x01]); final reader = BinaryReader(buffer); - expect(reader.readUint32, throwsRangeError); - expect(reader.readInt32, throwsRangeError); - expect(reader.readFloat32, throwsRangeError); + 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); + 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, throwsRangeError); - expect(reader.readInt64, throwsRangeError); - }); + expect(reader.readUint64, throwsA(isA())); + expect(reader.readInt64, throwsA(isA())); + }, + ); - test('skip beyond buffer throws RangeError', () { + test('skip beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.skip(3), throwsArgumentError); + expect(() => reader.skip(3), throwsA(isA())); }); test('read and verify multiple values sequentially', () { @@ -365,5 +401,569 @@ void main() { 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 index 9b1eb06..8fc2d79 100644 --- a/test/binary_writer_performance_test.dart +++ b/test/binary_writer_performance_test.dart @@ -1,13 +1,11 @@ import 'dart:typed_data'; import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:pro_binary/src/binary_writer.dart'; +import 'package:pro_binary/pro_binary.dart'; class BinaryWriterBenchmark extends BenchmarkBase { - BinaryWriterBenchmark(this.iterations) - : super('BinaryWriter performance test'); + BinaryWriterBenchmark() : super('BinaryWriter performance test'); - final int iterations; late final BinaryWriter writer; @override @@ -17,7 +15,7 @@ class BinaryWriterBenchmark extends BenchmarkBase { @override void run() { - for (var i = 0; i < iterations; i++) { + for (var i = 0; i < 1000; i++) { writer ..writeUint8(42) ..writeInt8(-42) @@ -30,13 +28,23 @@ class BinaryWriterBenchmark extends BenchmarkBase { ..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('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(1000).report(); + BinaryWriterBenchmark.main(); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index df2644f..18c9fc8 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -11,113 +11,113 @@ void main() { writer = BinaryWriter(); }); - test('takeBytes for empty', () { + test('should return empty list when takeBytes called on empty writer', () { expect(writer.takeBytes(), isEmpty); }); - test('writeUint8', () { + test('should write single Uint8 value correctly', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); }); - test('writeInt8 negative value', () { + test('should write negative Int8 value correctly', () { writer.writeInt8(-1); expect(writer.takeBytes(), [255]); }); - test('writeUint16 big-endian', () { + test('should write Uint16 in big-endian format', () { writer.writeUint16(256); expect(writer.takeBytes(), [1, 0]); }); - test('writeUint16 little-endian', () { + test('should write Uint16 in little-endian format', () { writer.writeUint16(256, Endian.little); expect(writer.takeBytes(), [0, 1]); }); - test('writeInt16 big-endian', () { + test('should write Int16 in big-endian format', () { writer.writeInt16(-1); expect(writer.takeBytes(), [255, 255]); }); - test('writeInt16 little-endian', () { + test('should write Int16 in little-endian format', () { writer.writeInt16(-32768, Endian.little); expect(writer.takeBytes(), [0, 128]); }); - test('writeUint32 big-endian', () { + test('should write Uint32 in big-endian format', () { writer.writeUint32(65536); expect(writer.takeBytes(), [0, 1, 0, 0]); }); - test('writeUint32 little-endian', () { + test('should write Uint32 in little-endian format', () { writer.writeUint32(65536, Endian.little); expect(writer.takeBytes(), [0, 0, 1, 0]); }); - test('writeInt32 big-endian', () { + test('should write Int32 in big-endian format', () { writer.writeInt32(-1); expect(writer.takeBytes(), [255, 255, 255, 255]); }); - test('writeInt32 little-endian', () { + test('should write Int32 in little-endian format', () { writer.writeInt32(-2147483648, Endian.little); expect(writer.takeBytes(), [0, 0, 0, 128]); }); - test('writeUint64 big-endian', () { + test('should write Uint64 in big-endian format', () { writer.writeUint64(4294967296); expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); }); - test('writeUint64 little-endian', () { + 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('writeInt64 big-endian', () { + test('should write Int64 in big-endian format', () { writer.writeInt64(-1); expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); }); - test('writeInt64 little-endian', () { + 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('writeFloat32 big-endian', () { + test('should write Float32 in big-endian format', () { writer.writeFloat32(3.1415927); expect(writer.takeBytes(), [64, 73, 15, 219]); }); - test('writeFloat32 little-endian', () { + test('should write Float32 in little-endian format', () { writer.writeFloat32(3.1415927, Endian.little); expect(writer.takeBytes(), [219, 15, 73, 64]); }); - test('writeFloat64 big-endian', () { + test('should write Float64 in big-endian format', () { writer.writeFloat64(3.141592653589793); expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); }); - test('writeFloat64 little-endian', () { + 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('writeBytes', () { + test('should write byte array correctly', () { writer.writeBytes([1, 2, 3, 4, 5]); expect(writer.takeBytes(), [1, 2, 3, 4, 5]); }); - test('writeString', () { + 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('complex memory allocation test', () { + test('should handle complex sequence of different data types', () { final writer = BinaryWriter() ..writeUint8(42) ..writeInt8(-42) @@ -152,19 +152,22 @@ void main() { expect(bytes, equals(expectedBytes)); }); - test('buffer should expand when size exceeds initial allocation', () { - for (var i = 0; i < 100; i++) { - writer.writeUint8(i); - } + 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)); - } - }); + final result = writer.takeBytes(); + expect(result.length, equals(100)); + for (var i = 0; i < 100; i++) { + expect(result[i], equals(i)); + } + }, + ); - test('reuse writer after takeBytes', () { + test('should allow reusing writer after takeBytes', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); @@ -172,7 +175,7 @@ void main() { expect(writer.takeBytes(), [2]); }); - test('write large data set', () { + test('should handle writing large data sets efficiently', () { final largeData = Uint8List.fromList( List.generate(10000, (i) => i % 256), ); @@ -185,7 +188,7 @@ void main() { expect(result, equals(largeData)); }); - test('bytesWritten returns correct number of bytes', () { + test('should track bytesWritten correctly', () { writer.writeUint8(1); expect(writer.bytesWritten, equals(1)); @@ -202,5 +205,1034 @@ void main() { 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())); + }); + + test('should throw AssertionError when Uint8 value exceeds 255', () { + expect(() => writer.writeUint8(256), throwsA(isA())); + }); + + test('should throw AssertionError when Int8 value is less than -128', () { + expect(() => writer.writeInt8(-129), throwsA(isA())); + }); + + test('should throw AssertionError when Int8 value exceeds 127', () { + expect(() => writer.writeInt8(128), throwsA(isA())); + }); + + test('should throw AssertionError when Uint16 value is negative', () { + expect(() => writer.writeUint16(-1), throwsA(isA())); + }); + + test('should throw AssertionError when Uint16 value exceeds 65535', () { + expect(() => writer.writeUint16(65536), throwsA(isA())); + }); + + test( + 'should throw AssertionError when Int16 value is less than -32768', + () { + expect( + () => writer.writeInt16(-32769), + throwsA(isA()), + ); + }, + ); + + test('should throw AssertionError when Int16 value exceeds 32767', () { + expect(() => writer.writeInt16(32768), throwsA(isA())); + }); + + test('should throw AssertionError when Uint32 value is negative', () { + expect(() => writer.writeUint32(-1), throwsA(isA())); + }); + + test( + 'should throw AssertionError when Uint32 value exceeds 4294967295', + () { + expect( + () => writer.writeUint32(4294967296), + throwsA(isA()), + ); + }, + ); + + test( + 'should throw AssertionError when Int32 value is less than -2147483648', + () { + expect( + () => writer.writeInt32(-2147483649), + throwsA(isA()), + ); + }, + ); + + test( + 'should throw AssertionError when Int32 value exceeds 2147483647', + () { + expect( + () => writer.writeInt32(2147483648), + throwsA(isA()), + ); + }, + ); + }); + + 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())); + }); + + 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])); + }); + }); }); }