├── .vscode ├── settings.json └── launch.json ├── README.md ├── .gitmodules ├── analysis_options.yaml ├── lib ├── src │ ├── reader │ │ ├── ext_decoder.dart │ │ ├── decoder.dart │ │ └── deserializer.dart │ ├── writer │ │ ├── ext_encoder.dart │ │ ├── encoder.dart │ │ ├── data_writer.dart │ │ └── serializer.dart │ ├── common │ │ ├── format_error.dart │ │ ├── float.dart │ │ ├── byte_data_extensions.dart │ │ ├── byte_data_extensions_js.dart │ │ └── msgpack_timestamp.dart │ └── codec.dart └── msgpack_codec.dart ├── test ├── test_utils_js.dart ├── test_utils.dart ├── common │ ├── float_test.dart │ └── msgpack_timestamp_test.dart ├── msgpack_test_suite_test.dart ├── msgpack_test_suite_test.g.dart └── msgpack_dart_test.dart ├── .github ├── dependabot.yaml └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .metadata ├── tool ├── setup_git_hooks.dart └── update_test_suite.dart ├── example └── example.dart ├── pubspec.yaml ├── .devcontainer └── devcontainer.json ├── LICENSE ├── CHANGELOG.md └── .gitignore /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "cSpell.words": [ 4 | "msgpack" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # msgpack_codec 2 | 3 | MessagePack implementation for dart. 4 | 5 | Clean, simple, fast and with sane API and implementation. 6 | 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/msgpack-test-suite"] 2 | path = test/msgpack-test-suite 3 | url = https://github.com/kawanet/msgpack-test-suite.git 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_test_tools/package.yaml 2 | 3 | linter: 4 | rules: 5 | cascade_invocations: false 6 | public_member_api_docs: false 7 | prefer_foreach: false 8 | 9 | -------------------------------------------------------------------------------- /lib/src/reader/ext_decoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | abstract class ExtDecoder { 4 | /// Return null if the data cannot be decoded 5 | dynamic decodeObject(int extType, Uint8List data); 6 | } 7 | -------------------------------------------------------------------------------- /test/test_utils_js.dart: -------------------------------------------------------------------------------- 1 | const uint64TestValue = 9007199254740991; 2 | const uint64Packed = [207, 0, 31, 255, 255, 255, 255, 255, 255]; 3 | 4 | const int64TestValue = -9007199254740991; 5 | const int64Packed = [211, 255, 224, 0, 0, 0, 0, 0, 1]; 6 | -------------------------------------------------------------------------------- /test/test_utils.dart: -------------------------------------------------------------------------------- 1 | const uint64TestValue = 9223372036854775807; 2 | const uint64Packed = [207, 127, 255, 255, 255, 255, 255, 255, 255]; 3 | 4 | const int64TestValue = -9223372036854775808; 5 | const int64Packed = [211, 128, 0, 0, 0, 0, 0, 0, 0]; 6 | -------------------------------------------------------------------------------- /lib/src/writer/ext_encoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | abstract interface class ExtEncoder { 4 | /// Return null if object can't be encoded 5 | int? extTypeForObject(dynamic object); 6 | 7 | Uint8List encodeObject(dynamic object); 8 | } 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pub" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /lib/src/common/format_error.dart: -------------------------------------------------------------------------------- 1 | // coverage:ignore-file 2 | 3 | class MsgpackFormatException implements Exception { 4 | MsgpackFormatException(this.message); 5 | 6 | final String message; 7 | 8 | @override 9 | String toString() => 'MsgpackFormatException: $message'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/msgpack_codec.dart: -------------------------------------------------------------------------------- 1 | export 'src/codec.dart'; 2 | export 'src/common/float.dart'; 3 | export 'src/common/format_error.dart'; 4 | export 'src/common/msgpack_timestamp.dart'; 5 | export 'src/reader/decoder.dart'; 6 | export 'src/reader/ext_decoder.dart'; 7 | export 'src/writer/encoder.dart'; 8 | export 'src/writer/ext_encoder.dart'; 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: d9ad220bbb17972913fb687f8d673d79b9f66dab 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | name: CD - Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | uses: Skycoder42/dart_test_tools/.github/workflows/publish.yml@main 12 | permissions: 13 | id-token: write 14 | with: 15 | withSubmodules: true 16 | buildRunner: false 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuos Integration 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | ci: 9 | name: CI 10 | uses: Skycoder42/dart_test_tools/.github/workflows/dart.yml@main 11 | with: 12 | withSubmodules: true 13 | unitTestPaths: test 14 | minCoverage: 90 15 | 16 | cd: 17 | name: CD 18 | needs: 19 | - ci 20 | uses: Skycoder42/dart_test_tools/.github/workflows/release.yml@main 21 | secrets: 22 | githubToken: ${{ secrets.GH_PAT }} 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Benchmark", 9 | "args": [ 10 | "-a" 11 | ], 12 | "program": "example/benchmark.dart", 13 | "request": "launch", 14 | "type": "dart" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /lib/src/common/float.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | @immutable 4 | final class Float { 5 | final double value; 6 | 7 | const Float(this.value); 8 | 9 | @override 10 | bool operator ==(Object other) { 11 | if (identical(this, other)) { 12 | return true; 13 | } else if (other is! Float) { 14 | return false; 15 | } else { 16 | return value == other.value; 17 | } 18 | } 19 | 20 | @override 21 | int get hashCode => value.hashCode; 22 | 23 | @override 24 | String toString() => value.toString(); 25 | } 26 | -------------------------------------------------------------------------------- /test/common/float_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_test_tools/test.dart'; 2 | import 'package:msgpack_codec/src/common/float.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('$Float', () { 7 | testData( 8 | 'equality wraps double', 9 | const [ 10 | (Float(1.1), Float(1.1), true), 11 | (Float(1.1), 1, false), 12 | (Float(1.1), Float(1.2), false), 13 | ], 14 | (fixture) { 15 | expect(fixture.$1 == fixture.$2, fixture.$3); 16 | expect(fixture.$1.hashCode == fixture.$2.hashCode, fixture.$3); 17 | }, 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /tool/setup_git_hooks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | Future main() async { 4 | final preCommitHook = File('.git/hooks/pre-commit'); 5 | await preCommitHook.parent.create(); 6 | await preCommitHook.writeAsString(''' 7 | #!/bin/sh 8 | exec dart run dart_pre_commit # specify custom options here 9 | # exec flutter pub run dart_pre_commit # Use this instead when working on a flutter project 10 | '''); 11 | 12 | if (!Platform.isWindows) { 13 | final result = await Process.run('chmod', ['a+x', preCommitHook.path]); 14 | stdout.write(result.stdout); 15 | stderr.write(result.stderr); 16 | exitCode = result.exitCode; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:typed_data'; 4 | 5 | import 'package:msgpack_codec/msgpack_codec.dart'; 6 | 7 | void main(List args) { 8 | final data = { 9 | 'name': 'John Doe', 10 | 'age': 30, 11 | 'isEmployed': true, 12 | 'children': [ 13 | {'name': 'Alice', 'age': 7}, 14 | {'name': 'Bob', 'age': 10}, 15 | ], 16 | }; 17 | 18 | final encoded = msgPack.encode(data); 19 | 20 | print('Encoded: ${_toHexString(encoded)}'); 21 | 22 | final decoded = msgPack.decode(encoded); 23 | 24 | print('Decoded: $decoded'); 25 | } 26 | 27 | String _toHexString(Uint8List bytes) => 28 | bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join('-'); 29 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: msgpack_codec 2 | description: A message pack implementation for dart that works on all platforms. 3 | version: 2.0.2 4 | homepage: https://github.com/Skycoder42/msgpack_codec 5 | 6 | environment: 7 | sdk: ^3.7.0 8 | 9 | dependencies: 10 | meta: ^1.16.0 11 | 12 | dev_dependencies: 13 | collection: ^1.19.1 14 | custom_lint: ^0.7.5 15 | dart_pre_commit: ^5.4.4 16 | dart_test_tools: ^6.1.1 17 | test: ^1.25.15 18 | 19 | dart_pre_commit: 20 | pull-up-dependencies: 21 | allowed: 22 | - meta 23 | 24 | cider: 25 | link_template: 26 | tag: https://github.com/Skycoder42/msgpack_codec/releases/tag/v%tag% # initial release link template 27 | diff: https://github.com/Skycoder42/msgpack_codec/compare/v%from%...v%to% # subsequent releases link template 28 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flutter (Stable)", 3 | "image": "skycoder42/devcontainers-flutter:latest", 4 | "customizations": { 5 | "vscode": { 6 | "extensions": [ 7 | "dart-code.dart-code", 8 | "github.vscode-github-actions", 9 | "mhutchie.git-graph", 10 | "redhat.vscode-yaml", 11 | "streetsidesoftware.code-spell-checker", 12 | "timonwong.shellcheck" 13 | ], 14 | "settings": { 15 | "dart.sdkPath": "/home/vscode/flutter/bin/cache/dart-sdk", 16 | "dart.flutterSdkPath": "/home/vscode/flutter", 17 | "terminal.integrated.defaultProfile.linux": "zsh" 18 | } 19 | } 20 | }, 21 | "features": { 22 | "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { 23 | "plugins": "git colorize vscode", 24 | "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions" 25 | }, 26 | "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tool/update_test_suite.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | Future main(List args) async { 5 | final testDataFile = File( 6 | 'test/msgpack-test-suite/dist/msgpack-test-suite.json', 7 | ); 8 | final testDataJson = await testDataFile.readAsString(); 9 | final condensedJson = json.encode(json.decode(testDataJson)); 10 | 11 | final outFile = File('test/msgpack_test_suite_test.g.dart'); 12 | await outFile.writeAsString(''' 13 | part of 'msgpack_test_suite_test.dart'; 14 | 15 | Map _loadTestData() => json.decode( 16 | // ignore: lines_longer_than_80_chars 17 | '$condensedJson', 18 | ) as Map; 19 | 20 | Future _validateUpToDate() async { 21 | final testDataFile = 22 | File('test/msgpack-test-suite/dist/msgpack-test-suite.json'); 23 | final testDataJson = await testDataFile.readAsString(); 24 | final testData = json.decode(testDataJson) as Map; 25 | expect(_loadTestData(), testData); 26 | } 27 | '''); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/codec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'reader/decoder.dart'; 5 | import 'reader/ext_decoder.dart'; 6 | import 'writer/encoder.dart'; 7 | import 'writer/ext_encoder.dart'; 8 | 9 | class MsgpackCodec extends Codec { 10 | final Encoding _codec; 11 | final ExtEncoder? _extEncoder; 12 | final ExtDecoder? _extDecoder; 13 | final bool copyBinaryData; 14 | 15 | const MsgpackCodec({ 16 | Encoding codec = utf8, 17 | ExtEncoder? extEncoder, 18 | ExtDecoder? extDecoder, 19 | this.copyBinaryData = false, 20 | }) : _codec = codec, 21 | _extEncoder = extEncoder, 22 | _extDecoder = extDecoder; 23 | 24 | @override 25 | Converter get encoder => 26 | MsgpackEncoder(codec: _codec, extEncoder: _extEncoder); 27 | 28 | @override 29 | Converter get decoder => MsgpackDecoder( 30 | codec: _codec, 31 | extDecoder: _extDecoder, 32 | copyBinaryData: copyBinaryData, 33 | ); 34 | } 35 | 36 | const msgPack = MsgpackCodec(); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matej Knopp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/writer/encoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import '../common/format_error.dart'; 5 | import 'data_writer.dart'; 6 | import 'ext_encoder.dart'; 7 | import 'serializer.dart'; 8 | 9 | class MsgpackEncoder extends Converter { 10 | final Encoding _codec; 11 | final ExtEncoder? _extEncoder; 12 | 13 | const MsgpackEncoder({Encoding codec = utf8, ExtEncoder? extEncoder}) 14 | : _codec = codec, 15 | _extEncoder = extEncoder; 16 | 17 | @override 18 | Uint8List convert(dynamic input) => _autoWrap(() { 19 | final writer = ByteBufferDataWriter(); 20 | final serializer = Serializer(writer, _codec, _extEncoder); 21 | serializer.encode(input); 22 | return writer.takeBytes(); 23 | }); 24 | 25 | @override 26 | Sink startChunkedConversion(Sink sink) { 27 | final writer = SinkDataWriter(sink); 28 | final serializer = Serializer(writer, _codec, _extEncoder); 29 | return _SerializerSink(writer, serializer); 30 | } 31 | } 32 | 33 | class _SerializerSink implements Sink { 34 | final SinkDataWriter _writer; 35 | final Serializer _serializer; 36 | 37 | _SerializerSink(this._writer, this._serializer); 38 | 39 | @override 40 | void add(dynamic data) => _autoWrap(() => _serializer.encode(data)); 41 | 42 | @override 43 | void close() => _autoWrap(_writer.finalize); 44 | } 45 | 46 | T _autoWrap(T Function() callback) { 47 | try { 48 | return callback(); 49 | } on MsgpackFormatException { 50 | rethrow; 51 | } catch (e, s) { 52 | Error.throwWithStackTrace(MsgpackFormatException(e.toString()), s); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.0.2] - 2025-03-16 8 | ### Changed 9 | - Updated dependencies 10 | - Updated min dart sdk to 3.7.0 11 | 12 | ## [2.0.1] - 2025-01-30 13 | ### Changed 14 | - Improved exception handling 15 | - encoder and decoder now wrap all exceptions as `MsgpackFormatException` 16 | 17 | ## [2.0.0+1] - 2025-01-18 18 | ### Changed 19 | - Initial automated release 20 | 21 | ## [2.0.0] - 2025-01-18 22 | ### Changed 23 | - Forked the library to archive add the following: 24 | - Dart JS support 25 | - Timestamp extension 26 | - message pack codec with chunked conversion 27 | - more and better tests 28 | - various code cleanups and modernizations 29 | - renamed to `msgpack_codec` 30 | 31 | ## 1.0.1 - 2023-01-12 32 | - Remove dependency on dart:io 33 | 34 | ## 1.0.0 - 2021-03-11 35 | - Migrated to null safety (thanks RootSoft) 36 | 37 | ## 0.0.7 - 2020-03-14 38 | - Fix wrong length when writing ext8 (0xc7) 39 | 40 | ## 0.0.6 - 2019-09-20 41 | - Accept any iterable when serializing, not just List 42 | - Accept ByteData when serializing (will be deserialized as Uint8List) 43 | 44 | ## 0.0.5 - 2019-05-19 45 | - Changed return value from `List` to `Uint8List`. 46 | 47 | [2.0.2]: https://github.com/Skycoder42/msgpack_codec/compare/v2.0.1...v2.0.2 48 | [2.0.1]: https://github.com/Skycoder42/msgpack_codec/compare/v2.0.0+1...v2.0.1 49 | [2.0.0+1]: https://github.com/Skycoder42/msgpack_codec/compare/v2.0.0...v2.0.0+1 50 | [2.0.0]: https://github.com/Skycoder42/msgpack_codec/releases/tag/v2.0.0 51 | -------------------------------------------------------------------------------- /lib/src/common/byte_data_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'format_error.dart'; 6 | 7 | @internal 8 | extension ByteDataExtensions on ByteData { 9 | @pragma('vm:prefer-inline') 10 | void setUint64Safe(int byteOffset, int value, [Endian endian = Endian.big]) => 11 | setUint64(byteOffset, value, endian); 12 | 13 | @pragma('vm:prefer-inline') 14 | void setInt64Safe(int byteOffset, int value, [Endian endian = Endian.big]) => 15 | setInt64(byteOffset, value, endian); 16 | 17 | @pragma('vm:prefer-inline') 18 | int getUint64Safe(int byteOffset, [Endian endian = Endian.big]) => 19 | getUint64(byteOffset, endian); 20 | 21 | @pragma('vm:prefer-inline') 22 | int getInt64Safe(int byteOffset, [Endian endian = Endian.big]) => 23 | getInt64(byteOffset, endian); 24 | 25 | void setBigUint64( 26 | int byteOffset, 27 | BigInt value, [ 28 | Endian endian = Endian.big, 29 | ]) { 30 | if (value.isValidInt) { 31 | setUint64(byteOffset, value.toUnsigned(64).toInt(), endian); 32 | return; 33 | } 34 | 35 | final signedValue = value.toSigned(value.bitLength); 36 | if (signedValue.isValidInt) { 37 | setUint64(byteOffset, signedValue.toInt(), endian); 38 | return; 39 | } 40 | 41 | throw MsgpackFormatException( 42 | 'Value is too big to be serialized as a 64 bit integer', 43 | ); 44 | } 45 | 46 | void setBigInt64(int byteOffset, BigInt value, [Endian endian = Endian.big]) { 47 | if (!value.isValidInt) { 48 | throw MsgpackFormatException( 49 | 'Value is too big/small to be serialized as a 64 bit integer', 50 | ); 51 | } 52 | setInt64(byteOffset, value.toSigned(64).toInt(), endian); 53 | } 54 | 55 | @pragma('vm:prefer-inline') 56 | BigInt getBigUint64(int byteOffset, [Endian endian = Endian.big]) => 57 | BigInt.from(getUint64(byteOffset, endian)).toUnsigned(64); 58 | 59 | @pragma('vm:prefer-inline') 60 | BigInt getBigInt64(int byteOffset, [Endian endian = Endian.big]) => 61 | BigInt.from(getInt64(byteOffset, endian)).toSigned(64); 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/common/byte_data_extensions_js.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'format_error.dart'; 6 | 7 | @internal 8 | extension ByteDataExtensions on ByteData { 9 | void setUint64Safe(int byteOffset, int value, [Endian endian = Endian.big]) { 10 | assert(!value.isNegative, 'Use setInt64Safe for negative integers'); 11 | 12 | final bigI = BigInt.from(value); 13 | _setBigInt64(byteOffset, bigI, endian); 14 | } 15 | 16 | void setInt64Safe(int byteOffset, int value, [Endian endian = Endian.big]) { 17 | final bigI = BigInt.from(value); 18 | _setBigInt64(byteOffset, bigI, endian); 19 | } 20 | 21 | int getUint64Safe(int byteOffset, [Endian endian = Endian.big]) { 22 | final bigI = getBigUint64(byteOffset, endian); 23 | if (!bigI.isValidInt) { 24 | throw MsgpackFormatException( 25 | 'Value is too big to be serialized as a 64 bit integer', 26 | ); 27 | } 28 | return bigI.toInt(); 29 | } 30 | 31 | int getInt64Safe(int byteOffset, [Endian endian = Endian.big]) { 32 | final bigI = getBigInt64(byteOffset, endian); 33 | if (!bigI.isValidInt) { 34 | throw MsgpackFormatException( 35 | 'Value is too big/small to be serialized as a 64 bit integer', 36 | ); 37 | } 38 | return bigI.toInt(); 39 | } 40 | 41 | @pragma('vm:prefer-inline') 42 | void setBigUint64( 43 | int byteOffset, 44 | BigInt value, [ 45 | Endian endian = Endian.big, 46 | ]) => _setBigInt64(byteOffset, value, endian); 47 | 48 | @pragma('vm:prefer-inline') 49 | void setBigInt64( 50 | int byteOffset, 51 | BigInt value, [ 52 | Endian endian = Endian.big, 53 | ]) => _setBigInt64(byteOffset, value, endian); 54 | 55 | @pragma('vm:prefer-inline') 56 | BigInt getBigUint64(int byteOffset, [Endian endian = Endian.big]) => 57 | _getBigInt64(byteOffset, endian).toUnsigned(64); 58 | 59 | @pragma('vm:prefer-inline') 60 | BigInt getBigInt64(int byteOffset, [Endian endian = Endian.big]) => 61 | _getBigInt64(byteOffset, endian).toSigned(64); 62 | 63 | void _setBigInt64( 64 | int byteOffset, 65 | BigInt value, [ 66 | Endian endian = Endian.big, 67 | ]) { 68 | if (value.bitLength > 64) { 69 | throw MsgpackFormatException( 70 | 'Value is too big/small to be serialized as a 64 bit integer', 71 | ); 72 | } 73 | 74 | var bigI = value; 75 | for (var i = 0; i < 8; i++) { 76 | final offset = endian == Endian.little ? i : 7 - i; 77 | setUint8(byteOffset + offset, bigI.toUnsigned(8).toInt()); 78 | bigI >>= 8; 79 | } 80 | } 81 | 82 | BigInt _getBigInt64(int byteOffset, [Endian endian = Endian.big]) { 83 | var bigI = BigInt.zero; 84 | for (var i = 0; i < 8; i++) { 85 | final offset = endian == Endian.big ? i : 7 - i; 86 | final byte = getUint8(byteOffset + offset); 87 | bigI <<= 8; 88 | bigI += BigInt.from(byte); 89 | } 90 | return bigI; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/reader/decoder.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import '../common/format_error.dart'; 5 | import 'deserializer.dart'; 6 | import 'ext_decoder.dart'; 7 | 8 | class MsgpackDecoder extends Converter { 9 | final Encoding _codec; 10 | final ExtDecoder? _extDecoder; 11 | final bool copyBinaryData; 12 | 13 | const MsgpackDecoder({ 14 | Encoding codec = utf8, 15 | ExtDecoder? extDecoder, 16 | this.copyBinaryData = false, 17 | }) : _codec = codec, 18 | _extDecoder = extDecoder; 19 | 20 | @override 21 | dynamic convert(Uint8List input) { 22 | try { 23 | final deserializer = Deserializer( 24 | input, 25 | _codec, 26 | extDecoder: _extDecoder, 27 | copyBinaryData: copyBinaryData, 28 | ); 29 | return deserializer.decode(); 30 | } on MsgpackFormatException { 31 | rethrow; 32 | } catch (e, s) { 33 | Error.throwWithStackTrace(MsgpackFormatException(e.toString()), s); 34 | } 35 | } 36 | 37 | @override 38 | Sink startChunkedConversion(Sink sink) => 39 | _DecoderWrappingSink(this, sink); 40 | } 41 | 42 | class _DecoderWrappingSink implements Sink { 43 | final MsgpackDecoder _decoder; 44 | final Sink _sink; 45 | 46 | final _buffer = BytesBuilder(); 47 | int _offset = 0; 48 | 49 | _DecoderWrappingSink(this._decoder, this._sink); 50 | 51 | @override 52 | void add(Uint8List data) { 53 | _buffer.add(data); 54 | _flush(); 55 | } 56 | 57 | @override 58 | void close() { 59 | if (!_flush()) { 60 | throw MsgpackFormatException('Unexpected end of input'); 61 | } 62 | _sink.close(); 63 | } 64 | 65 | bool _flush() { 66 | final bytes = _buffer.toBytes(); 67 | try { 68 | final deserializer = Deserializer( 69 | bytes, 70 | initialOffset: _offset, 71 | _decoder._codec, 72 | extDecoder: _decoder._extDecoder, 73 | copyBinaryData: _decoder.copyBinaryData, 74 | ); 75 | while (_offset < _buffer.length) { 76 | _sink.add(deserializer.decode()); 77 | _offset = deserializer.currentOffset; 78 | } 79 | 80 | return true; 81 | } on MsgpackFormatException { 82 | rethrow; 83 | // ignore: avoid_catching_errors 84 | } on RangeError { 85 | return false; 86 | // ignore: avoid_catching_errors 87 | } on ArgumentError { 88 | return false; 89 | } catch (e, s) { 90 | Error.throwWithStackTrace(MsgpackFormatException(e.toString()), s); 91 | } finally { 92 | if (_offset == _buffer.length) { 93 | // if the buffer was fully consumed, clear it 94 | _buffer.clear(); 95 | _offset = 0; 96 | } else if (_buffer.length >= 1024 * 1024 * 10) { 97 | // if the buffer is larger than 10MB, clear it 98 | // and preserve the remaining data 99 | _buffer.clear(); 100 | _buffer.add(bytes.sublist(_offset)); 101 | _offset = 0; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | ios/.generated/ 9 | ios/Flutter/Generated.xcconfig 10 | ios/Runner/GeneratedPluginRegistrant.* 11 | 12 | pubspec.lock 13 | 14 | 15 | # Created by https://www.toptal.com/developers/gitignore/api/phpstorm 16 | # Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm 17 | 18 | ### PhpStorm ### 19 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 20 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 21 | 22 | # User-specific stuff 23 | .idea/**/workspace.xml 24 | .idea/**/tasks.xml 25 | .idea/**/usage.statistics.xml 26 | .idea/**/dictionaries 27 | .idea/**/shelf 28 | 29 | # Generated files 30 | .idea/**/contentModel.xml 31 | 32 | # Sensitive or high-churn files 33 | .idea/**/dataSources/ 34 | .idea/**/dataSources.ids 35 | .idea/**/dataSources.local.xml 36 | .idea/**/sqlDataSources.xml 37 | .idea/**/dynamic.xml 38 | .idea/**/uiDesigner.xml 39 | .idea/**/dbnavigator.xml 40 | 41 | # Gradle 42 | .idea/**/gradle.xml 43 | .idea/**/libraries 44 | 45 | # Gradle and Maven with auto-import 46 | # When using Gradle or Maven with auto-import, you should exclude module files, 47 | # since they will be recreated, and may cause churn. Uncomment if using 48 | # auto-import. 49 | # .idea/artifacts 50 | # .idea/compiler.xml 51 | # .idea/jarRepositories.xml 52 | # .idea/modules.xml 53 | # .idea/*.iml 54 | # .idea/modules 55 | # *.iml 56 | # *.ipr 57 | 58 | # CMake 59 | cmake-build-*/ 60 | 61 | # Mongo Explorer plugin 62 | .idea/**/mongoSettings.xml 63 | 64 | # File-based project format 65 | *.iws 66 | 67 | # IntelliJ 68 | out/ 69 | 70 | # mpeltonen/sbt-idea plugin 71 | .idea_modules/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Cursive Clojure plugin 77 | .idea/replstate.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | fabric.properties 84 | 85 | # Editor-based Rest Client 86 | .idea/httpRequests 87 | 88 | # Android studio 3.1+ serialized cache file 89 | .idea/caches/build_file_checksums.ser 90 | 91 | ### PhpStorm Patch ### 92 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 93 | 94 | # *.iml 95 | # modules.xml 96 | # .idea/misc.xml 97 | # *.ipr 98 | 99 | # Sonarlint plugin 100 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 101 | .idea/**/sonarlint/ 102 | 103 | # SonarQube Plugin 104 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 105 | .idea/**/sonarIssues.xml 106 | 107 | # Markdown Navigator plugin 108 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 109 | .idea/**/markdown-navigator.xml 110 | .idea/**/markdown-navigator-enh.xml 111 | .idea/**/markdown-navigator/ 112 | 113 | # Cache file creation bug 114 | # See https://youtrack.jetbrains.com/issue/JBR-2257 115 | .idea/$CACHE_FILE$ 116 | 117 | # CodeStream plugin 118 | # https://plugins.jetbrains.com/plugin/12206-codestream 119 | .idea/codestream.xml 120 | 121 | # End of https://www.toptal.com/developers/gitignore/api/phpstorm 122 | 123 | custom_lint.log 124 | -------------------------------------------------------------------------------- /lib/src/common/msgpack_timestamp.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | // coverage:ignore-start 4 | class TimestampTruncatedException implements Exception { 5 | final String message; 6 | 7 | TimestampTruncatedException(this.message); 8 | 9 | @override 10 | String toString() => 'TimestampTruncatedException: $message'; 11 | } 12 | // coverage:ignore-end 13 | 14 | @immutable 15 | class MsgpackTimestamp implements Comparable { 16 | static final _dateTimeOffsetMicroSecondsMax = 17 | // 100000000 * 24 * 60 * 60 * 1000 * 1000 18 | BigInt.parse('8640000000000000000'); 19 | static final _nanosPerSecond = BigInt.from(1000000000); 20 | 21 | static final zero = MsgpackTimestamp(BigInt.zero); 22 | 23 | final BigInt seconds; 24 | final BigInt nanoSeconds; 25 | 26 | MsgpackTimestamp(this.seconds, [BigInt? nanoSeconds]) 27 | : nanoSeconds = nanoSeconds ?? BigInt.zero, 28 | assert(seconds.bitLength <= 64, 'seconds must be at most 64 bits'), 29 | assert( 30 | nanoSeconds == null || !nanoSeconds.isNegative, 31 | 'nanoseconds must be positive', 32 | ), 33 | assert( 34 | nanoSeconds == null || nanoSeconds.bitLength <= 32, 35 | 'nanoseconds must be at most 32 bits', 36 | ); 37 | 38 | factory MsgpackTimestamp.fromNanoSecondsSinceEpoch(BigInt nanoSeconds) { 39 | var seconds = nanoSeconds ~/ _nanosPerSecond; 40 | final nanoSecondsRemainder = nanoSeconds % _nanosPerSecond; 41 | if (nanoSeconds.isNegative && nanoSecondsRemainder > BigInt.zero) { 42 | seconds -= BigInt.one; 43 | } 44 | return MsgpackTimestamp(seconds, nanoSecondsRemainder); 45 | } 46 | 47 | factory MsgpackTimestamp.fromDateTime(DateTime dateTime) => 48 | MsgpackTimestamp.fromNanoSecondsSinceEpoch( 49 | BigInt.from(dateTime.microsecondsSinceEpoch) * BigInt.from(1000), 50 | ); 51 | 52 | BigInt get nanoSecondsSinceEpoch => seconds * _nanosPerSecond + nanoSeconds; 53 | 54 | DateTime toDateTime({bool truncate = false}) { 55 | if (!truncate && nanoSeconds % BigInt.from(1000) != BigInt.zero) { 56 | throw TimestampTruncatedException( 57 | 'Cannot convert timestamp with nanoseconds to DateTime', 58 | ); 59 | } 60 | 61 | var microSeconds = nanoSecondsSinceEpoch ~/ BigInt.from(1000); 62 | if (microSeconds.abs() >= _dateTimeOffsetMicroSecondsMax) { 63 | if (truncate) { 64 | microSeconds = 65 | _dateTimeOffsetMicroSecondsMax * BigInt.from(microSeconds.sign); 66 | } else { 67 | throw TimestampTruncatedException( 68 | 'Cannot convert timestamp with more than 100000000 days epoch offset', 69 | ); 70 | } 71 | } 72 | 73 | // coverage:ignore-start 74 | if (!microSeconds.isValidInt) { 75 | throw TimestampTruncatedException( 76 | 'Timestamp cannot be represented as integer', 77 | ); 78 | } 79 | // coverage:ignore-end 80 | 81 | final dateTime = DateTime.fromMicrosecondsSinceEpoch( 82 | microSeconds.toInt(), 83 | isUtc: true, 84 | ); 85 | return dateTime; 86 | } 87 | 88 | @override 89 | int compareTo(MsgpackTimestamp other) => 90 | nanoSecondsSinceEpoch.compareTo(other.nanoSecondsSinceEpoch); 91 | 92 | @override 93 | bool operator ==(Object other) { 94 | if (identical(this, other)) { 95 | return true; 96 | } else if (other is! MsgpackTimestamp) { 97 | return false; 98 | } else { 99 | return seconds == other.seconds && nanoSeconds == other.nanoSeconds; 100 | } 101 | } 102 | 103 | @override 104 | int get hashCode => Object.hash(seconds, nanoSeconds); 105 | 106 | @override 107 | String toString() => 'MsgpackTimestamp($seconds, $nanoSeconds)'; 108 | } 109 | -------------------------------------------------------------------------------- /test/common/msgpack_timestamp_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unnecessary_lambdas 2 | 3 | import 'package:dart_test_tools/test.dart'; 4 | import 'package:msgpack_codec/src/common/msgpack_timestamp.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('$MsgpackTimestamp', () { 9 | group('constructor', () { 10 | test('asserts if seconds are too large', () { 11 | expect( 12 | () => MsgpackTimestamp(BigInt.one << 64), 13 | throwsA(isA()), 14 | ); 15 | expect( 16 | () => MsgpackTimestamp(-BigInt.one << 65), 17 | throwsA(isA()), 18 | ); 19 | }); 20 | 21 | test('asserts if nanoseconds are negative', () { 22 | expect( 23 | () => MsgpackTimestamp(BigInt.one, -BigInt.one), 24 | throwsA(isA()), 25 | ); 26 | }); 27 | 28 | test('asserts if nanoseconds are too large', () { 29 | expect( 30 | () => MsgpackTimestamp(BigInt.one, BigInt.one << 32), 31 | throwsA(isA()), 32 | ); 33 | }); 34 | }); 35 | 36 | group('nanoSecondsSinceEpoch conversion', () { 37 | testData( 38 | 'converts nanoseconds to timestamp and back', 39 | [ 40 | (BigInt.from(0), BigInt.from(0), BigInt.from(0)), 41 | (BigInt.from(1), BigInt.from(0), BigInt.from(1)), 42 | (BigInt.from(-1), BigInt.from(-1), BigInt.from(999999999)), 43 | (BigInt.from(999999999), BigInt.from(0), BigInt.from(999999999)), 44 | (BigInt.from(-999999999), BigInt.from(-1), BigInt.from(1)), 45 | (BigInt.from(1000000000), BigInt.from(1), BigInt.from(0)), 46 | (BigInt.from(-1000000000), BigInt.from(-1), BigInt.from(0)), 47 | (BigInt.from(1000000001), BigInt.from(1), BigInt.from(1)), 48 | (BigInt.from(-1000000001), BigInt.from(-2), BigInt.from(999999999)), 49 | ( 50 | BigInt.from(555444333222111), 51 | BigInt.from(555444), 52 | BigInt.from(333222111), 53 | ), 54 | ( 55 | BigInt.from(-555444333222111), 56 | BigInt.from(-555445), 57 | BigInt.from(666777889), 58 | ), 59 | ], 60 | (fixture) { 61 | final (en, ts, tn) = fixture; 62 | final timestamp = MsgpackTimestamp.fromNanoSecondsSinceEpoch(en); 63 | expect(timestamp.seconds, equals(ts)); 64 | expect(timestamp.nanoSeconds, equals(tn)); 65 | final restoreNanos = timestamp.nanoSecondsSinceEpoch; 66 | expect(restoreNanos, en); 67 | }, 68 | ); 69 | }); 70 | 71 | group('datetime conversion', () { 72 | testData( 73 | 'converts DateTime to timestamp and back', 74 | [ 75 | (DateTime.utc(1970), BigInt.from(0), BigInt.from(0)), 76 | (DateTime.utc(0), BigInt.from(-62167219200), BigInt.from(0)), 77 | ( 78 | DateTime.utc(0, 1, 2, 3, 4, 5, 6, 8), 79 | BigInt.from(-62167121755), 80 | BigInt.from(6008000), 81 | ), 82 | ( 83 | DateTime.utc(2024, 12, 10, 16, 38, 17, 44, 55), 84 | BigInt.from(1733848697), 85 | BigInt.from(44055000), 86 | ), 87 | ], 88 | (fixture) { 89 | final (dt, ts, tn) = fixture; 90 | final timestamp = MsgpackTimestamp.fromDateTime(dt); 91 | expect(timestamp.seconds, equals(ts)); 92 | expect(timestamp.nanoSeconds, equals(tn)); 93 | final restoredDt = timestamp.toDateTime(); 94 | expect(restoredDt.isUtc, isTrue); 95 | expect(restoredDt, dt); 96 | expect( 97 | timestamp.nanoSecondsSinceEpoch, 98 | BigInt.from(dt.microsecondsSinceEpoch) * BigInt.from(1000), 99 | ); 100 | }, 101 | ); 102 | 103 | test('correctly handles timestamps with nanosecond values', () { 104 | final ts = MsgpackTimestamp(BigInt.one, BigInt.from(123456789)); 105 | expect( 106 | () => ts.toDateTime(), 107 | throwsA(isA()), 108 | ); 109 | expect( 110 | ts.toDateTime(truncate: true), 111 | DateTime.utc(1970, 1, 1, 0, 0, 1, 123, 456), 112 | ); 113 | }); 114 | 115 | test('correctly handles timestamps with very large timestamps', () { 116 | final tsPos = MsgpackTimestamp(BigInt.one << 63); 117 | expect( 118 | () => tsPos.toDateTime(), 119 | throwsA(isA()), 120 | ); 121 | expect(tsPos.toDateTime(truncate: true), DateTime.utc(275760, 9, 13)); 122 | 123 | final tsNeg = MsgpackTimestamp(-BigInt.one << 63); 124 | expect( 125 | () => tsNeg.toDateTime(), 126 | throwsA(isA()), 127 | ); 128 | expect(tsNeg.toDateTime(truncate: true), DateTime.utc(-271821, 4, 20)); 129 | }); 130 | }); 131 | 132 | testData( 133 | 'compareTo correctly compares timestamps', 134 | [ 135 | ( 136 | MsgpackTimestamp(BigInt.from(10)), 137 | MsgpackTimestamp(BigInt.from(10)), 138 | 0, 139 | ), 140 | ( 141 | MsgpackTimestamp(BigInt.from(10)), 142 | MsgpackTimestamp(BigInt.from(20)), 143 | -1, 144 | ), 145 | ( 146 | MsgpackTimestamp(BigInt.from(20)), 147 | MsgpackTimestamp(BigInt.from(10)), 148 | 1, 149 | ), 150 | ], 151 | (fixture) { 152 | expect(fixture.$1.compareTo(fixture.$2).sign, fixture.$3); 153 | }, 154 | ); 155 | 156 | testData( 157 | 'equality operator works correctly', 158 | [ 159 | (MsgpackTimestamp.zero, MsgpackTimestamp.zero, true), 160 | (MsgpackTimestamp.zero, BigInt.zero, false), 161 | (MsgpackTimestamp(BigInt.one), MsgpackTimestamp(BigInt.one), true), 162 | ( 163 | MsgpackTimestamp(BigInt.one, BigInt.one), 164 | MsgpackTimestamp(BigInt.one, BigInt.two), 165 | false, 166 | ), 167 | ], 168 | (fixture) { 169 | expect(fixture.$1 == fixture.$2, fixture.$3); 170 | expect(fixture.$1.hashCode == fixture.$2.hashCode, fixture.$3); 171 | }, 172 | ); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/writer/data_writer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../common/byte_data_extensions.dart' 6 | if (dart.library.js_interop) '../common/byte_data_extensions_js.dart'; 7 | 8 | const int _kScratchSizeInitial = 64; 9 | const int _kScratchSizeRegular = 1024; 10 | 11 | @internal 12 | abstract base class DataWriterBase { 13 | Uint8List? _scratchBuffer; 14 | ByteData? scratchData; 15 | int scratchOffset = 0; 16 | 17 | void writeUint8(int i) { 18 | ensureSize(1); 19 | scratchData?.setUint8(scratchOffset, i); 20 | scratchOffset += 1; 21 | } 22 | 23 | void writeInt8(int i) { 24 | ensureSize(1); 25 | scratchData?.setInt8(scratchOffset, i); 26 | scratchOffset += 1; 27 | } 28 | 29 | void writeUint16(int i, [Endian endian = Endian.big]) { 30 | ensureSize(2); 31 | scratchData?.setUint16(scratchOffset, i, endian); 32 | scratchOffset += 2; 33 | } 34 | 35 | void writeInt16(int i, [Endian endian = Endian.big]) { 36 | ensureSize(2); 37 | scratchData?.setInt16(scratchOffset, i, endian); 38 | scratchOffset += 2; 39 | } 40 | 41 | void writeUint32(int i, [Endian endian = Endian.big]) { 42 | ensureSize(4); 43 | scratchData?.setUint32(scratchOffset, i, endian); 44 | scratchOffset += 4; 45 | } 46 | 47 | void writeInt32(int i, [Endian endian = Endian.big]) { 48 | ensureSize(4); 49 | scratchData?.setInt32(scratchOffset, i, endian); 50 | scratchOffset += 4; 51 | } 52 | 53 | void writeUint64(int i, [Endian endian = Endian.big]) { 54 | ensureSize(8); 55 | scratchData?.setUint64Safe(scratchOffset, i, endian); 56 | scratchOffset += 8; 57 | } 58 | 59 | void writeInt64(int i, [Endian endian = Endian.big]) { 60 | ensureSize(8); 61 | scratchData?.setInt64Safe(scratchOffset, i, endian); 62 | scratchOffset += 8; 63 | } 64 | 65 | void writeBigUint64(BigInt i, [Endian endian = Endian.big]) { 66 | ensureSize(8); 67 | scratchData?.setBigUint64(scratchOffset, i, endian); 68 | scratchOffset += 8; 69 | } 70 | 71 | void writeBigInt64(BigInt i, [Endian endian = Endian.big]) { 72 | ensureSize(8); 73 | scratchData?.setBigInt64(scratchOffset, i, endian); 74 | scratchOffset += 8; 75 | } 76 | 77 | void writeFloat32(double f, [Endian endian = Endian.big]) { 78 | ensureSize(4); 79 | scratchData?.setFloat32(scratchOffset, f, endian); 80 | scratchOffset += 4; 81 | } 82 | 83 | void writeFloat64(double f, [Endian endian = Endian.big]) { 84 | ensureSize(8); 85 | scratchData?.setFloat64(scratchOffset, f, endian); 86 | scratchOffset += 8; 87 | } 88 | 89 | // The list may be retained until takeBytes is called 90 | void writeBytes(List bytes) { 91 | final length = bytes.length; 92 | if (length == 0) { 93 | return; 94 | } 95 | ensureSize(length); 96 | if (scratchOffset == 0) { 97 | // we can add it directly 98 | _appendBytes(bytes); 99 | } else { 100 | // there is enough room in _scratchBuffer, otherwise _ensureSize 101 | // would have added _scratchBuffer to _builder and _scratchOffset would 102 | // be 0 103 | if (bytes is Uint8List) { 104 | _scratchBuffer?.setRange(scratchOffset, scratchOffset + length, bytes); 105 | } else { 106 | for (var i = 0; i < length; i++) { 107 | _scratchBuffer?[scratchOffset + i] = bytes[i]; 108 | } 109 | } 110 | scratchOffset += length; 111 | } 112 | } 113 | 114 | void ensureSize(int size) { 115 | if (_scratchBuffer == null) { 116 | // start with small scratch buffer, expand to regular later if needed 117 | _scratchBuffer = Uint8List(_kScratchSizeInitial); 118 | scratchData = ByteData.view( 119 | _scratchBuffer!.buffer, 120 | _scratchBuffer!.offsetInBytes, 121 | ); 122 | } 123 | final remaining = _scratchBuffer!.length - scratchOffset; 124 | if (remaining < size) { 125 | _appendScratchBuffer(); 126 | } 127 | } 128 | 129 | void _appendScratchBuffer({bool finalize = false}) { 130 | if (scratchOffset > 0) { 131 | if (_scratchBuffer!.length == _kScratchSizeInitial) { 132 | // We're still on small scratch buffer, move it to _builder 133 | // and create regular one 134 | _appendBytes( 135 | Uint8List.view( 136 | _scratchBuffer!.buffer, 137 | _scratchBuffer!.offsetInBytes, 138 | scratchOffset, 139 | ), 140 | ); 141 | 142 | // Don't create new scratch buffer if we're finalizing 143 | if (finalize) { 144 | return; 145 | } 146 | 147 | _scratchBuffer = Uint8List(_kScratchSizeRegular); 148 | scratchData = ByteData.view( 149 | _scratchBuffer!.buffer, 150 | _scratchBuffer!.offsetInBytes, 151 | ); 152 | } else { 153 | _appendBytes( 154 | Uint8List.fromList( 155 | Uint8List.view( 156 | _scratchBuffer!.buffer, 157 | _scratchBuffer!.offsetInBytes, 158 | scratchOffset, 159 | ), 160 | ), 161 | ); 162 | } 163 | scratchOffset = 0; 164 | } 165 | } 166 | 167 | void _appendBytes(List bytes); 168 | } 169 | 170 | @internal 171 | final class ByteBufferDataWriter extends DataWriterBase { 172 | final _builder = BytesBuilder(copy: false); 173 | 174 | Uint8List takeBytes() { 175 | if (_builder.isEmpty) { 176 | // Just take scratch data 177 | final res = Uint8List.view( 178 | _scratchBuffer!.buffer, 179 | _scratchBuffer!.offsetInBytes, 180 | scratchOffset, 181 | ); 182 | scratchOffset = 0; 183 | _scratchBuffer = null; 184 | scratchData = null; 185 | return res; 186 | } else { 187 | _appendScratchBuffer(); 188 | return _builder.takeBytes(); 189 | } 190 | } 191 | 192 | @override 193 | void _appendBytes(List bytes) => _builder.add(bytes); 194 | } 195 | 196 | @internal 197 | final class SinkDataWriter extends DataWriterBase { 198 | final Sink sink; 199 | 200 | SinkDataWriter(this.sink); 201 | 202 | void finalize() { 203 | _appendScratchBuffer(); 204 | sink.close(); 205 | } 206 | 207 | @override 208 | void _appendBytes(List bytes) => 209 | sink.add(bytes is Uint8List ? bytes : Uint8List.fromList(bytes)); 210 | } 211 | -------------------------------------------------------------------------------- /test/msgpack_test_suite_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: discarded_futures 2 | 3 | import 'dart:convert'; 4 | import 'dart:io'; 5 | import 'dart:math'; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:collection/collection.dart'; 9 | import 'package:meta/meta.dart'; 10 | import 'package:msgpack_codec/src/codec.dart'; 11 | import 'package:msgpack_codec/src/common/msgpack_timestamp.dart'; 12 | import 'package:msgpack_codec/src/reader/ext_decoder.dart'; 13 | import 'package:msgpack_codec/src/writer/ext_encoder.dart'; 14 | import 'package:test/test.dart'; 15 | 16 | part 'msgpack_test_suite_test.g.dart'; 17 | 18 | @immutable 19 | class TestSuiteExt { 20 | final int id; 21 | final Uint8List bytes; 22 | 23 | const TestSuiteExt(this.id, this.bytes); 24 | 25 | @override 26 | bool operator ==(Object other) => 27 | other is TestSuiteExt && 28 | id == other.id && 29 | const ListEquality().equals(bytes, other.bytes); 30 | 31 | @override 32 | int get hashCode => Object.hash(id, bytes); 33 | 34 | @override 35 | String toString() => 'TestSuiteExt(id: $id, bytes: $bytes)'; 36 | } 37 | 38 | class TestSuiteExtEncoder implements ExtEncoder { 39 | const TestSuiteExtEncoder(); 40 | 41 | @override 42 | int? extTypeForObject(dynamic object) => switch (object) { 43 | TestSuiteExt(:final id) => id, 44 | _ => null, 45 | }; 46 | 47 | @override 48 | Uint8List encodeObject(covariant TestSuiteExt object) => object.bytes; 49 | } 50 | 51 | class TestSuiteExtDecoder implements ExtDecoder { 52 | const TestSuiteExtDecoder(); 53 | 54 | @override 55 | TestSuiteExt decodeObject(int id, Uint8List bytes) => TestSuiteExt(id, bytes); 56 | } 57 | 58 | Future main() async { 59 | test('validate test data is up to date', _validateUpToDate, testOn: 'vm'); 60 | 61 | final testData = _loadTestData(); 62 | for (final MapEntry(key: testSuite, value: List testCases) 63 | in testData.entries) { 64 | group(testSuite, () { 65 | for (final testCase in testCases.cast>()) { 66 | final msgpack = (testCase['msgpack'] as List).cast(); 67 | 68 | switch (testCase) { 69 | case {'nil': _}: 70 | _testValue(null, msgpack); 71 | case {'bool': final bool value}: 72 | _testValue(value, msgpack); 73 | case {'binary': final String value}: 74 | _testValue(_hexToBytes(value), msgpack.cast()); 75 | case {'bignum': final String value}: 76 | final bigInt = BigInt.parse(value); 77 | _testValue( 78 | bigInt.isValidInt ? bigInt.toInt() : bigInt, 79 | msgpack, 80 | skip: 81 | bigInt.isValidInt 82 | ? null 83 | : '$bigInt cannot be converted to int', 84 | ); 85 | case {'number': final num value}: 86 | _testValue(value, msgpack); 87 | case {'string': final String value}: 88 | _testValue(value, msgpack); 89 | case {'array': final List value}: 90 | _testValue(value, msgpack); 91 | case {'map': final Map value}: 92 | _testValue(value, msgpack); 93 | case {'timestamp': [final int seconds, final int nanoSeconds]}: 94 | final timestamp = MsgpackTimestamp( 95 | BigInt.from(seconds), 96 | BigInt.from(nanoSeconds), 97 | ); 98 | _testValue(timestamp, msgpack); 99 | case {'ext': [final int id, final String bytes]}: 100 | _testValue(TestSuiteExt(id, _hexToBytes(bytes)), msgpack); 101 | default: 102 | throw UnimplementedError('Unknown test case: $testCase'); 103 | } 104 | } 105 | }); 106 | } 107 | } 108 | 109 | const _testCodec = MsgpackCodec( 110 | extEncoder: TestSuiteExtEncoder(), 111 | extDecoder: TestSuiteExtDecoder(), 112 | ); 113 | 114 | void _testValue(dynamic value, List msgpack, {Object? skip}) { 115 | _testEncode(value, msgpack, skip: skip); 116 | _testEncodeChunked(value, msgpack, skip: skip); 117 | _testDecode(value, msgpack, skip: skip); 118 | _testDecodeChunked(value, msgpack, skip: skip); 119 | } 120 | 121 | void _testEncode(dynamic value, List msgpack, {Object? skip}) { 122 | test('serializes $value to any of $msgpack', skip: skip, () { 123 | final encoded = _testCodec.encode(value); 124 | expect(encoded, _hexEqualsAny(msgpack)); 125 | }); 126 | } 127 | 128 | void _testEncodeChunked(dynamic value, List msgpack, {Object? skip}) { 129 | test('serializes $value to any of $msgpack (chunked)', skip: skip, () { 130 | final encoded = 131 | Stream.value( 132 | value, 133 | ).transform(_testCodec.encoder).expand((chunk) => chunk).toList(); 134 | expect(encoded, completion(_hexEqualsAny(msgpack))); 135 | }); 136 | } 137 | 138 | void _testDecode(dynamic value, List msgpack, {Object? skip}) { 139 | group('deserializes $value from', skip: skip, () { 140 | for (final representation in msgpack) { 141 | test('"$representation"', () { 142 | final decoded = _testCodec.decode(_hexToBytes(representation)); 143 | expect(decoded, value); 144 | }); 145 | } 146 | }); 147 | } 148 | 149 | final _rng = Random.secure(); 150 | 151 | void _testDecodeChunked(dynamic value, List msgpack, {Object? skip}) { 152 | group('deserializes $value from', skip: skip, () { 153 | for (final representation in msgpack) { 154 | test('"$representation" (chunked)', () { 155 | final bytes = _hexToBytes(representation); 156 | final chunks = []; 157 | var offset = 0; 158 | while (offset < bytes.length) { 159 | final chunkSize = _rng.nextInt(bytes.length - offset) + 1; 160 | chunks.add(bytes.sublist(offset, offset + chunkSize)); 161 | offset += chunkSize; 162 | } 163 | 164 | final decoded = 165 | Stream.fromIterable(chunks).transform(_testCodec.decoder).single; 166 | expect(decoded, completion(value)); 167 | }); 168 | } 169 | }); 170 | } 171 | 172 | Matcher _hexEqualsAny(Iterable hexRepresentations) => 173 | _AnyOf(hexRepresentations.map(_hexEquals).toList()); 174 | 175 | Matcher _hexEquals(String hexRepresentation) => 176 | orderedEquals(_hexToBytes(hexRepresentation)); 177 | 178 | Uint8List _hexToBytes(String hexRepresentation) => 179 | hexRepresentation.isEmpty 180 | ? Uint8List(0) 181 | : Uint8List.fromList( 182 | hexRepresentation 183 | .split('-') 184 | .map((byte) => int.parse(byte, radix: 16)) 185 | .toList(), 186 | ); 187 | 188 | class _AnyOf extends Matcher { 189 | final List _matchers; 190 | 191 | const _AnyOf(this._matchers); 192 | 193 | @override 194 | bool matches(dynamic item, Map matchState) { 195 | for (final matcher in _matchers) { 196 | if (matcher.matches(item, matchState)) { 197 | return true; 198 | } 199 | } 200 | return false; 201 | } 202 | 203 | @override 204 | Description describe(Description description) => 205 | description.addAll('(', ' or ', ')', _matchers); 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/reader/deserializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:meta/meta.dart'; 5 | 6 | import '../common/byte_data_extensions.dart' 7 | if (dart.library.js_interop) '../common/byte_data_extensions_js.dart'; 8 | import '../common/format_error.dart'; 9 | import '../common/msgpack_timestamp.dart'; 10 | import 'ext_decoder.dart'; 11 | 12 | @internal 13 | class Deserializer { 14 | final ExtDecoder? _extDecoder; 15 | final Encoding _codec; 16 | final Uint8List _list; 17 | final ByteData _data; 18 | int _offset; 19 | 20 | Deserializer( 21 | this._list, 22 | this._codec, { 23 | ExtDecoder? extDecoder, 24 | this.copyBinaryData = false, 25 | int initialOffset = 0, 26 | }) : _data = ByteData.view(_list.buffer, _list.offsetInBytes), 27 | _extDecoder = extDecoder, 28 | _offset = initialOffset; 29 | 30 | /// If false, decoded binary data buffers will reference underlying input 31 | /// buffer and thus may change when the content of input buffer changes. 32 | /// 33 | /// If true, decoded buffers are copies and the underlying input buffer is 34 | /// free to change after decoding. 35 | final bool copyBinaryData; 36 | 37 | int get currentOffset => _offset; 38 | 39 | dynamic decode() { 40 | final u = _list[_offset++]; 41 | if (u <= 127) { 42 | return u; 43 | } else if ((u & 0xE0) == 0xE0) { 44 | // negative small integer 45 | return u - 256; 46 | } else if ((u & 0xE0) == 0xA0) { 47 | return _readString(u & 0x1F); 48 | } else if ((u & 0xF0) == 0x90) { 49 | return _readArray(u & 0xF); 50 | } else if ((u & 0xF0) == 0x80) { 51 | return _readMap(u & 0xF); 52 | } 53 | switch (u) { 54 | case 0xc0: 55 | return null; 56 | case 0xc2: 57 | return false; 58 | case 0xc3: 59 | return true; 60 | case 0xcc: 61 | return _readUInt8(); 62 | case 0xcd: 63 | return _readUInt16(); 64 | case 0xce: 65 | return _readUInt32(); 66 | case 0xcf: 67 | return _readUInt64(); 68 | case 0xd0: 69 | return _readInt8(); 70 | case 0xd1: 71 | return _readInt16(); 72 | case 0xd2: 73 | return _readInt32(); 74 | case 0xd3: 75 | return _readInt64(); 76 | case 0xca: 77 | return _readFloat(); 78 | case 0xcb: 79 | return _readDouble(); 80 | case 0xd9: 81 | return _readString(_readUInt8()); 82 | case 0xda: 83 | return _readString(_readUInt16()); 84 | case 0xdb: 85 | return _readString(_readUInt32()); 86 | case 0xc4: 87 | return _readBuffer(_readUInt8()); 88 | case 0xc5: 89 | return _readBuffer(_readUInt16()); 90 | case 0xc6: 91 | return _readBuffer(_readUInt32()); 92 | case 0xdc: 93 | return _readArray(_readUInt16()); 94 | case 0xdd: 95 | return _readArray(_readUInt32()); 96 | case 0xde: 97 | return _readMap(_readUInt16()); 98 | case 0xdf: 99 | return _readMap(_readUInt32()); 100 | case 0xd4: 101 | return _readExt(1); 102 | case 0xd5: 103 | return _readExt(2); 104 | case 0xd6: 105 | return _readExt(4); 106 | case 0xd7: 107 | return _readExt(8); 108 | case 0xd8: 109 | return _readExt(16); 110 | case 0xc7: 111 | return _readExt(_readUInt8()); 112 | case 0xc8: 113 | return _readExt(_readUInt16()); 114 | case 0xc9: 115 | return _readExt(_readUInt32()); 116 | default: 117 | throw MsgpackFormatException('Invalid MessagePack format'); 118 | } 119 | } 120 | 121 | int _readInt8() => _data.getInt8(_offset++); 122 | 123 | int _readUInt8() => _data.getUint8(_offset++); 124 | 125 | int _readUInt16() { 126 | final res = _data.getUint16(_offset); 127 | _offset += 2; 128 | return res; 129 | } 130 | 131 | int _readInt16() { 132 | final res = _data.getInt16(_offset); 133 | _offset += 2; 134 | return res; 135 | } 136 | 137 | int _readUInt32() { 138 | final res = _data.getUint32(_offset); 139 | _offset += 4; 140 | return res; 141 | } 142 | 143 | int _readInt32() { 144 | final res = _data.getInt32(_offset); 145 | _offset += 4; 146 | return res; 147 | } 148 | 149 | int _readUInt64() { 150 | final res = _data.getUint64Safe(_offset); 151 | _offset += 8; 152 | return res; 153 | } 154 | 155 | int _readInt64() { 156 | final res = _data.getInt64Safe(_offset); 157 | _offset += 8; 158 | return res; 159 | } 160 | 161 | BigInt _readBigUInt64() { 162 | final res = _data.getBigUint64(_offset); 163 | _offset += 8; 164 | return res; 165 | } 166 | 167 | BigInt _readBigInt64() { 168 | final res = _data.getBigInt64(_offset); 169 | _offset += 8; 170 | return res; 171 | } 172 | 173 | double _readFloat() { 174 | final res = _data.getFloat32(_offset); 175 | _offset += 4; 176 | return res; 177 | } 178 | 179 | double _readDouble() { 180 | final res = _data.getFloat64(_offset); 181 | _offset += 8; 182 | return res; 183 | } 184 | 185 | Uint8List _readBuffer(int length) { 186 | final res = Uint8List.view( 187 | _list.buffer, 188 | _list.offsetInBytes + _offset, 189 | length, 190 | ); 191 | _offset += length; 192 | return copyBinaryData ? Uint8List.fromList(res) : res; 193 | } 194 | 195 | String _readString(int length) { 196 | final list = _readBuffer(length); 197 | final len = list.length; 198 | for (var i = 0; i < len; ++i) { 199 | if (list[i] > 127) { 200 | return _codec.decode(list); 201 | } 202 | } 203 | return String.fromCharCodes(list); 204 | } 205 | 206 | List _readArray(int length) { 207 | final res = List.filled(length, null); 208 | for (var i = 0; i < length; ++i) { 209 | res[i] = decode(); 210 | } 211 | return res; 212 | } 213 | 214 | Map _readMap(int length) { 215 | final res = {}; 216 | var remainingLength = length; 217 | while (remainingLength > 0) { 218 | res[decode()] = decode(); 219 | --remainingLength; 220 | } 221 | return res; 222 | } 223 | 224 | dynamic _readExt(int length) => switch (_readUInt8()) { 225 | 0xFF => _readTimestamp(length), 226 | final extType => _readCustomExt(extType, length), 227 | }; 228 | 229 | MsgpackTimestamp _readTimestamp(int length) { 230 | switch (length) { 231 | case 4: 232 | return MsgpackTimestamp(BigInt.from(_readUInt32())); 233 | case 8: 234 | final value = _readBigUInt64(); 235 | final nanoSeconds = value >> 34; 236 | final seconds = value & BigInt.from(0x00000003ffffffff); 237 | return MsgpackTimestamp(seconds, nanoSeconds); 238 | case 12: 239 | final nanoSeconds = _readUInt32(); 240 | final seconds = _readBigInt64(); 241 | return MsgpackTimestamp(seconds, BigInt.from(nanoSeconds)); 242 | default: 243 | return throw MsgpackFormatException( 244 | 'Unexpected timestamp length $length. Must be 4, 8 or 12 bytes', 245 | ); 246 | } 247 | } 248 | 249 | dynamic _readCustomExt(int extType, int length) { 250 | final data = _readBuffer(length); 251 | final decoded = _extDecoder?.decodeObject(extType, data); 252 | if (decoded != null) { 253 | return decoded; 254 | } 255 | throw MsgpackFormatException( 256 | "Don't know how to deserialize " 257 | '0x${extType.toRadixString(16).padLeft(2, '0')}', 258 | ); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lib/src/writer/serializer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:meta/meta.dart'; 5 | 6 | import '../common/float.dart'; 7 | import '../common/format_error.dart'; 8 | import '../common/msgpack_timestamp.dart'; 9 | import 'data_writer.dart'; 10 | import 'ext_encoder.dart'; 11 | 12 | @internal 13 | class Serializer { 14 | final DataWriterBase _writer; 15 | final Encoding _codec; 16 | final ExtEncoder? _extEncoder; 17 | 18 | Serializer(this._writer, this._codec, this._extEncoder); 19 | 20 | void encode(dynamic d) { 21 | if (d == null) return _writer.writeUint8(0xc0); 22 | if (d is bool) return _writer.writeUint8(d ? 0xc3 : 0xc2); 23 | if (d is int) return d >= 0 ? _writePositiveInt(d) : _writeNegativeInt(d); 24 | if (d is Float) return _writeFloat(d); 25 | if (d is double) return _writeDouble(d); 26 | if (d is String) return _writeString(d); 27 | if (d is Uint8List) return _writeBinary(d); 28 | if (d is Iterable) return _writeIterable(d); 29 | if (d is ByteData) { 30 | return _writeBinary( 31 | d.buffer.asUint8List(d.offsetInBytes, d.lengthInBytes), 32 | ); 33 | } 34 | if (d is Map) return _writeMap(d); 35 | if (_extEncoder != null && _writeExt(d)) { 36 | return; 37 | } 38 | // run built-in extensions AFTER the custom ones 39 | if (d is MsgpackTimestamp) return _writeTimeStamp(d); 40 | throw MsgpackFormatException("Don't know how to serialize $d"); 41 | } 42 | 43 | void _writeNegativeInt(int n) { 44 | if (n >= -32) { 45 | _writer.writeInt8(n); 46 | } else if (n >= -128) { 47 | _writer.writeUint8(0xd0); 48 | _writer.writeInt8(n); 49 | } else if (n >= -32768) { 50 | _writer.writeUint8(0xd1); 51 | _writer.writeInt16(n); 52 | } else if (n >= -2147483648) { 53 | _writer.writeUint8(0xd2); 54 | _writer.writeInt32(n); 55 | } else { 56 | _writer.writeUint8(0xd3); 57 | _writer.writeInt64(n); 58 | } 59 | } 60 | 61 | void _writePositiveInt(int n) { 62 | if (n <= 127) { 63 | _writer.writeUint8(n); 64 | } else if (n <= 0xFF) { 65 | _writer.writeUint8(0xcc); 66 | _writer.writeUint8(n); 67 | } else if (n <= 0xFFFF) { 68 | _writer.writeUint8(0xcd); 69 | _writer.writeUint16(n); 70 | } else if (n <= 0xFFFFFFFF) { 71 | _writer.writeUint8(0xce); 72 | _writer.writeUint32(n); 73 | } else { 74 | _writer.writeUint8(0xcf); 75 | _writer.writeUint64(n); 76 | } 77 | } 78 | 79 | void _writeFloat(Float n) { 80 | _writer.writeUint8(0xca); 81 | _writer.writeFloat32(n.value); 82 | } 83 | 84 | void _writeDouble(double n) { 85 | _writer.writeUint8(0xcb); 86 | _writer.writeFloat64(n); 87 | } 88 | 89 | void _writeString(String s) { 90 | final encoded = _codec.encode(s); 91 | final length = encoded.length; 92 | if (length <= 31) { 93 | _writer.writeUint8(0xA0 | length); 94 | } else if (length <= 0xFF) { 95 | _writer.writeUint8(0xd9); 96 | _writer.writeUint8(length); 97 | } else if (length <= 0xFFFF) { 98 | _writer.writeUint8(0xda); 99 | _writer.writeUint16(length); 100 | } else if (length <= 0xFFFFFFFF) { 101 | _writer.writeUint8(0xdb); 102 | _writer.writeUint32(length); 103 | } else { 104 | throw MsgpackFormatException( 105 | 'String is too long to be serialized with msgpack.', 106 | ); 107 | } 108 | _writer.writeBytes(encoded); 109 | } 110 | 111 | void _writeBinary(Uint8List buffer) { 112 | final length = buffer.length; 113 | if (length <= 0xFF) { 114 | _writer.writeUint8(0xc4); 115 | _writer.writeUint8(length); 116 | } else if (length <= 0xFFFF) { 117 | _writer.writeUint8(0xc5); 118 | _writer.writeUint16(length); 119 | } else if (length <= 0xFFFFFFFF) { 120 | _writer.writeUint8(0xc6); 121 | _writer.writeUint32(length); 122 | } else { 123 | throw MsgpackFormatException( 124 | 'Data is too long to be serialized with msgpack.', 125 | ); 126 | } 127 | _writer.writeBytes(buffer); 128 | } 129 | 130 | void _writeIterable(Iterable iterable) { 131 | final length = iterable.length; 132 | 133 | if (length <= 0xF) { 134 | _writer.writeUint8(0x90 | length); 135 | } else if (length <= 0xFFFF) { 136 | _writer.writeUint8(0xdc); 137 | _writer.writeUint16(length); 138 | } else if (length <= 0xFFFFFFFF) { 139 | _writer.writeUint8(0xdd); 140 | _writer.writeUint32(length); 141 | } else { 142 | throw MsgpackFormatException( 143 | 'Array is too big to be serialized with msgpack', 144 | ); 145 | } 146 | 147 | for (final item in iterable) { 148 | encode(item); 149 | } 150 | } 151 | 152 | void _writeMap(Map map) { 153 | final length = map.length; 154 | 155 | if (length <= 0xF) { 156 | _writer.writeUint8(0x80 | length); 157 | } else if (length <= 0xFFFF) { 158 | _writer.writeUint8(0xde); 159 | _writer.writeUint16(length); 160 | } else if (length <= 0xFFFFFFFF) { 161 | _writer.writeUint8(0xdf); 162 | _writer.writeUint32(length); 163 | } else { 164 | throw MsgpackFormatException( 165 | 'Map is too big to be serialized with msgpack', 166 | ); 167 | } 168 | 169 | for (final item in map.entries) { 170 | encode(item.key); 171 | encode(item.value); 172 | } 173 | } 174 | 175 | void _writeTimeStamp(MsgpackTimestamp timestamp) { 176 | final MsgpackTimestamp(:seconds, :nanoSeconds) = timestamp; 177 | 178 | if (seconds < BigInt.zero || seconds.bitLength > 34) { 179 | // value is too big for timestamp64 180 | _writer.writeBytes(const [0xc7, 12, 0xff]); 181 | _writer.writeUint32(nanoSeconds.toInt()); 182 | _writer.writeBigInt64(seconds); 183 | } else { 184 | if (seconds.bitLength > 32 || nanoSeconds != BigInt.zero) { 185 | // value is too big or too precise for timestamp32 186 | _writer.writeBytes(const [0xd7, 0xff]); 187 | _writer.writeBigUint64((nanoSeconds << 34) | seconds); 188 | } else { 189 | _writer.writeBytes(const [0xd6, 0xff]); 190 | _writer.writeUint32(seconds.toInt()); 191 | } 192 | } 193 | } 194 | 195 | bool _writeExt(dynamic object) { 196 | final type = _extEncoder?.extTypeForObject(object); 197 | if (type != null) { 198 | if (type < 0) { 199 | throw MsgpackFormatException('Negative ext type is reserved'); 200 | } 201 | final encoded = _extEncoder?.encodeObject(object); 202 | if (encoded == null) { 203 | throw MsgpackFormatException( 204 | 'Unable to encode object. No Encoder specified.', 205 | ); 206 | } 207 | 208 | final length = encoded.length; 209 | if (length == 1) { 210 | _writer.writeUint8(0xd4); 211 | } else if (length == 2) { 212 | _writer.writeUint8(0xd5); 213 | } else if (length == 4) { 214 | _writer.writeUint8(0xd6); 215 | } else if (length == 8) { 216 | _writer.writeUint8(0xd7); 217 | } else if (length == 16) { 218 | _writer.writeUint8(0xd8); 219 | } else if (length <= 0xFF) { 220 | _writer.writeUint8(0xc7); 221 | _writer.writeUint8(length); 222 | } else if (length <= 0xFFFF) { 223 | _writer.writeUint8(0xc8); 224 | _writer.writeUint16(length); 225 | } else if (length <= 0xFFFFFFFF) { 226 | _writer.writeUint8(0xc9); 227 | _writer.writeUint32(length); 228 | } else { 229 | throw MsgpackFormatException('Size must be at most 0xFFFFFFFF'); 230 | } 231 | _writer.writeUint8(type); 232 | _writer.writeBytes(encoded); 233 | return true; 234 | } 235 | return false; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /test/msgpack_test_suite_test.g.dart: -------------------------------------------------------------------------------- 1 | part of 'msgpack_test_suite_test.dart'; 2 | 3 | Map _loadTestData() => 4 | json.decode( 5 | // ignore: lines_longer_than_80_chars 6 | '{"10.nil.yaml":[{"nil":null,"msgpack":["c0"]}],"11.bool.yaml":[{"bool":false,"msgpack":["c2"]},{"bool":true,"msgpack":["c3"]}],"12.binary.yaml":[{"binary":"","msgpack":["c4-00","c5-00-00","c6-00-00-00-00"]},{"binary":"01","msgpack":["c4-01-01","c5-00-01-01","c6-00-00-00-01-01"]},{"binary":"00-ff","msgpack":["c4-02-00-ff","c5-00-02-00-ff","c6-00-00-00-02-00-ff"]}],"20.number-positive.yaml":[{"number":0,"msgpack":["00","cc-00","cd-00-00","ce-00-00-00-00","cf-00-00-00-00-00-00-00-00","d0-00","d1-00-00","d2-00-00-00-00","d3-00-00-00-00-00-00-00-00","ca-00-00-00-00","cb-00-00-00-00-00-00-00-00"]},{"number":1,"msgpack":["01","cc-01","cd-00-01","ce-00-00-00-01","cf-00-00-00-00-00-00-00-01","d0-01","d1-00-01","d2-00-00-00-01","d3-00-00-00-00-00-00-00-01","ca-3f-80-00-00","cb-3f-f0-00-00-00-00-00-00"]},{"number":127,"msgpack":["7f","cc-7f","cd-00-7f","ce-00-00-00-7f","cf-00-00-00-00-00-00-00-7f","d0-7f","d1-00-7f","d2-00-00-00-7f","d3-00-00-00-00-00-00-00-7f"]},{"number":128,"msgpack":["cc-80","cd-00-80","ce-00-00-00-80","cf-00-00-00-00-00-00-00-80","d1-00-80","d2-00-00-00-80","d3-00-00-00-00-00-00-00-80"]},{"number":255,"msgpack":["cc-ff","cd-00-ff","ce-00-00-00-ff","cf-00-00-00-00-00-00-00-ff","d1-00-ff","d2-00-00-00-ff","d3-00-00-00-00-00-00-00-ff"]},{"number":256,"msgpack":["cd-01-00","ce-00-00-01-00","cf-00-00-00-00-00-00-01-00","d1-01-00","d2-00-00-01-00","d3-00-00-00-00-00-00-01-00"]},{"number":65535,"msgpack":["cd-ff-ff","ce-00-00-ff-ff","cf-00-00-00-00-00-00-ff-ff","d2-00-00-ff-ff","d3-00-00-00-00-00-00-ff-ff"]},{"number":65536,"msgpack":["ce-00-01-00-00","cf-00-00-00-00-00-01-00-00","d2-00-01-00-00","d3-00-00-00-00-00-01-00-00"]},{"number":2147483647,"msgpack":["ce-7f-ff-ff-ff","cf-00-00-00-00-7f-ff-ff-ff","d2-7f-ff-ff-ff","d3-00-00-00-00-7f-ff-ff-ff"]},{"number":2147483648,"msgpack":["ce-80-00-00-00","cf-00-00-00-00-80-00-00-00","d3-00-00-00-00-80-00-00-00","ca-4f-00-00-00","cb-41-e0-00-00-00-00-00-00"]},{"number":4294967295,"msgpack":["ce-ff-ff-ff-ff","cf-00-00-00-00-ff-ff-ff-ff","d3-00-00-00-00-ff-ff-ff-ff","cb-41-ef-ff-ff-ff-e0-00-00"]}],"21.number-negative.yaml":[{"number":-1,"msgpack":["ff","d0-ff","d1-ff-ff","d2-ff-ff-ff-ff","d3-ff-ff-ff-ff-ff-ff-ff-ff","ca-bf-80-00-00","cb-bf-f0-00-00-00-00-00-00"]},{"number":-32,"msgpack":["e0","d0-e0","d1-ff-e0","d2-ff-ff-ff-e0","d3-ff-ff-ff-ff-ff-ff-ff-e0","ca-c2-00-00-00","cb-c0-40-00-00-00-00-00-00"]},{"number":-33,"msgpack":["d0-df","d1-ff-df","d2-ff-ff-ff-df","d3-ff-ff-ff-ff-ff-ff-ff-df"]},{"number":-128,"msgpack":["d0-80","d1-ff-80","d2-ff-ff-ff-80","d3-ff-ff-ff-ff-ff-ff-ff-80"]},{"number":-256,"msgpack":["d1-ff-00","d2-ff-ff-ff-00","d3-ff-ff-ff-ff-ff-ff-ff-00"]},{"number":-32768,"msgpack":["d1-80-00","d2-ff-ff-80-00","d3-ff-ff-ff-ff-ff-ff-80-00"]},{"number":-65536,"msgpack":["d2-ff-ff-00-00","d3-ff-ff-ff-ff-ff-ff-00-00"]},{"number":-2147483648,"msgpack":["d2-80-00-00-00","d3-ff-ff-ff-ff-80-00-00-00","cb-c1-e0-00-00-00-00-00-00"]}],"22.number-float.yaml":[{"number":0.5,"msgpack":["ca-3f-00-00-00","cb-3f-e0-00-00-00-00-00-00"]},{"number":-0.5,"msgpack":["ca-bf-00-00-00","cb-bf-e0-00-00-00-00-00-00"]}],"23.number-bignum.yaml":[{"number":4294967296,"bignum":"4294967296","msgpack":["cf-00-00-00-01-00-00-00-00","d3-00-00-00-01-00-00-00-00","ca-4f-80-00-00","cb-41-f0-00-00-00-00-00-00"]},{"number":-4294967296,"bignum":"-4294967296","msgpack":["d3-ff-ff-ff-ff-00-00-00-00","cb-c1-f0-00-00-00-00-00-00"]},{"number":281474976710656,"bignum":"281474976710656","msgpack":["cf-00-01-00-00-00-00-00-00","d3-00-01-00-00-00-00-00-00","ca-57-80-00-00","cb-42-f0-00-00-00-00-00-00"]},{"number":-281474976710656,"bignum":"-281474976710656","msgpack":["d3-ff-ff-00-00-00-00-00-00","ca-d7-80-00-00","cb-c2-f0-00-00-00-00-00-00"]},{"bignum":"9223372036854775807","msgpack":["d3-7f-ff-ff-ff-ff-ff-ff-ff","cf-7f-ff-ff-ff-ff-ff-ff-ff"]},{"bignum":"-9223372036854775807","msgpack":["d3-80-00-00-00-00-00-00-01"]},{"bignum":"9223372036854775808","msgpack":["cf-80-00-00-00-00-00-00-00"]},{"bignum":"-9223372036854775808","msgpack":["d3-80-00-00-00-00-00-00-00"]},{"bignum":"18446744073709551615","msgpack":["cf-ff-ff-ff-ff-ff-ff-ff-ff"]}],"30.string-ascii.yaml":[{"string":"","msgpack":["a0","d9-00","da-00-00","db-00-00-00-00"]},{"string":"a","msgpack":["a1-61","d9-01-61","da-00-01-61","db-00-00-00-01-61"]},{"string":"1234567890123456789012345678901","msgpack":["bf-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31","d9-1f-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31","da-00-1f-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31"]},{"string":"12345678901234567890123456789012","msgpack":["d9-20-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32","da-00-20-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32-33-34-35-36-37-38-39-30-31-32"]}],"31.string-utf8.yaml":[{"string":"Кириллица","msgpack":["b2-d0-9a-d0-b8-d1-80-d0-b8-d0-bb-d0-bb-d0-b8-d1-86-d0-b0","d9-12-d0-9a-d0-b8-d1-80-d0-b8-d0-bb-d0-bb-d0-b8-d1-86-d0-b0"]},{"string":"ひらがな","msgpack":["ac-e3-81-b2-e3-82-89-e3-81-8c-e3-81-aa","d9-0c-e3-81-b2-e3-82-89-e3-81-8c-e3-81-aa"]},{"string":"한글","msgpack":["a6-ed-95-9c-ea-b8-80","d9-06-ed-95-9c-ea-b8-80"]},{"string":"汉字","msgpack":["a6-e6-b1-89-e5-ad-97","d9-06-e6-b1-89-e5-ad-97"]},{"string":"漢字","msgpack":["a6-e6-bc-a2-e5-ad-97","d9-06-e6-bc-a2-e5-ad-97"]}],"32.string-emoji.yaml":[{"string":"❤","msgpack":["a3-e2-9d-a4","d9-03-e2-9d-a4"]},{"string":"🍺","msgpack":["a4-f0-9f-8d-ba","d9-04-f0-9f-8d-ba"]}],"40.array.yaml":[{"array":[],"msgpack":["90","dc-00-00","dd-00-00-00-00"]},{"array":[1],"msgpack":["91-01","dc-00-01-01","dd-00-00-00-01-01"]},{"array":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"msgpack":["9f-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f","dc-00-0f-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f","dd-00-00-00-0f-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f"]},{"array":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"msgpack":["dc-00-10-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f-10","dd-00-00-00-10-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f-10"]},{"array":["a"],"msgpack":["91-a1-61","dc-00-01-a1-61","dd-00-00-00-01-a1-61"]}],"41.map.yaml":[{"map":{},"msgpack":["80","de-00-00","df-00-00-00-00"]},{"map":{"a":1},"msgpack":["81-a1-61-01","de-00-01-a1-61-01","df-00-00-00-01-a1-61-01"]},{"map":{"a":"A"},"msgpack":["81-a1-61-a1-41","de-00-01-a1-61-a1-41","df-00-00-00-01-a1-61-a1-41"]}],"42.nested.yaml":[{"array":[[]],"msgpack":["91-90","dc-00-01-dc-00-00","dd-00-00-00-01-dd-00-00-00-00"]},{"array":[{}],"msgpack":["91-80","dc-00-01-80","dd-00-00-00-01-80"]},{"map":{"a":{}},"msgpack":["81-a1-61-80","de-00-01-a1-61-de-00-00","df-00-00-00-01-a1-61-df-00-00-00-00"]},{"map":{"a":[]},"msgpack":["81-a1-61-90","de-00-01-a1-61-90","df-00-00-00-01-a1-61-90"]}],"50.timestamp.yaml":[{"timestamp":[1514862245,0],"msgpack":["d6-ff-5a-4a-f6-a5"]},{"timestamp":[1514862245,678901234],"msgpack":["d7-ff-a1-dc-d7-c8-5a-4a-f6-a5"]},{"timestamp":[2147483647,999999999],"msgpack":["d7-ff-ee-6b-27-fc-7f-ff-ff-ff"]},{"timestamp":[2147483648,0],"msgpack":["d6-ff-80-00-00-00"]},{"timestamp":[2147483648,1],"msgpack":["d7-ff-00-00-00-04-80-00-00-00"]},{"timestamp":[4294967295,0],"msgpack":["d6-ff-ff-ff-ff-ff"]},{"timestamp":[4294967295,999999999],"msgpack":["d7-ff-ee-6b-27-fc-ff-ff-ff-ff"]},{"timestamp":[4294967296,0],"msgpack":["d7-ff-00-00-00-01-00-00-00-00"]},{"timestamp":[17179869183,999999999],"msgpack":["d7-ff-ee-6b-27-ff-ff-ff-ff-ff"]},{"timestamp":[17179869184,0],"msgpack":["c7-0c-ff-00-00-00-00-00-00-00-04-00-00-00-00"]},{"timestamp":[-1,0],"msgpack":["c7-0c-ff-00-00-00-00-ff-ff-ff-ff-ff-ff-ff-ff"]},{"timestamp":[-1,999999999],"msgpack":["c7-0c-ff-3b-9a-c9-ff-ff-ff-ff-ff-ff-ff-ff-ff"]},{"timestamp":[0,0],"msgpack":["d6-ff-00-00-00-00"]},{"timestamp":[0,1],"msgpack":["d7-ff-00-00-00-04-00-00-00-00"]},{"timestamp":[1,0],"msgpack":["d6-ff-00-00-00-01"]},{"timestamp":[-2208988801,999999999],"msgpack":["c7-0c-ff-3b-9a-c9-ff-ff-ff-ff-ff-7c-55-81-7f"]},{"timestamp":[-2208988800,0],"msgpack":["c7-0c-ff-00-00-00-00-ff-ff-ff-ff-7c-55-81-80"]},{"timestamp":[-62167219200,0],"msgpack":["c7-0c-ff-00-00-00-00-ff-ff-ff-f1-86-8b-84-00"]},{"timestamp":[253402300799,999999999],"msgpack":["c7-0c-ff-3b-9a-c9-ff-00-00-00-3a-ff-f4-41-7f"]}],"60.ext.yaml":[{"ext":[1,"10"],"msgpack":["d4-01-10"]},{"ext":[2,"20-21"],"msgpack":["d5-02-20-21"]},{"ext":[3,"30-31-32-33"],"msgpack":["d6-03-30-31-32-33"]},{"ext":[4,"40-41-42-43-44-45-46-47"],"msgpack":["d7-04-40-41-42-43-44-45-46-47"]},{"ext":[5,"50-51-52-53-54-55-56-57-58-59-5a-5b-5c-5d-5e-5f"],"msgpack":["d8-05-50-51-52-53-54-55-56-57-58-59-5a-5b-5c-5d-5e-5f"]},{"ext":[6,""],"msgpack":["c7-00-06","c8-00-00-06","c9-00-00-00-00-06"]},{"ext":[7,"70-71-72"],"msgpack":["c7-03-07-70-71-72","c8-00-03-07-70-71-72","c9-00-00-00-03-07-70-71-72"]}]}', 7 | ) 8 | as Map; 9 | 10 | Future _validateUpToDate() async { 11 | final testDataFile = File( 12 | 'test/msgpack-test-suite/dist/msgpack-test-suite.json', 13 | ); 14 | final testDataJson = await testDataFile.readAsString(); 15 | final testData = json.decode(testDataJson) as Map; 16 | expect(_loadTestData(), testData); 17 | } 18 | -------------------------------------------------------------------------------- /test/msgpack_dart_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:async'; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:msgpack_codec/src/codec.dart'; 7 | import 'package:msgpack_codec/src/common/float.dart'; 8 | import 'package:msgpack_codec/src/common/msgpack_timestamp.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'test_utils.dart' if (dart.library.js_interop) 'test_utils_js.dart'; 12 | 13 | // 14 | // Tests taken from msgpack2 (https://github.com/butlermatt/msgpack2) 15 | // 16 | 17 | Matcher isString = predicate((e) => e is String, 'is a String'); 18 | Matcher isInt = predicate((e) => e is int, 'is an int'); 19 | Matcher isMap = predicate((e) => e is Map, 'is a Map'); 20 | Matcher isList = predicate((e) => e is List, 'is a List'); 21 | 22 | void main() { 23 | test('Test Pack null', packNull); 24 | 25 | group('Test Pack Boolean', () { 26 | test('Pack boolean false', packFalse); 27 | test('Pack boolean true', packTrue); 28 | }); 29 | 30 | group('Test Pack Ints', () { 31 | test('Pack Positive FixInt', packPositiveFixInt); 32 | test('Pack Negative FixInt', packFixedNegative); 33 | test('Pack Uint8', packUint8); 34 | test('Pack Uint16', packUint16); 35 | test('Pack Uint32', packUint32); 36 | test('Pack Uint64', packUint64); 37 | test('Pack Int8', packInt8); 38 | test('Pack Int16', packInt16); 39 | test('Pack Int32', packInt32); 40 | test('Pack Int64', packInt64); 41 | }); 42 | 43 | group('Test Pack Floats', () { 44 | test('Pack Float32', packFloat32); 45 | test('Pack Float64 (double)', packDouble); 46 | }); 47 | 48 | test('Pack 5-character string', packString5); 49 | test('Pack 22-character string', packString22); 50 | test('Pack 256-character string', packString256); 51 | test('Pack 70000-character string', packString70000); 52 | test('Pack string array', packStringArray); 53 | test('Pack int-to-string map', packIntToStringMap); 54 | 55 | group('Test Pack Binary', () { 56 | test('Pack Bin8', packBin8); 57 | test('Pack Bin16', packBin16); 58 | test('Pack Bin32', packBin32); 59 | test('Pack ByteData', packByteData); 60 | }); 61 | 62 | group('Test Pack Extensions', () { 63 | test('Pack timestamp32', packTimestamp32); 64 | test('Pack timestamp64', packTimestamp64); 65 | test('Pack timestamp96', packTimestamp96); 66 | }); 67 | 68 | test('Test Pack chunked', packChunked); 69 | 70 | test('Test Unpack Null', unpackNull); 71 | 72 | group('Test Unpack boolean', () { 73 | test('Unpack boolean false', unpackFalse); 74 | test('Unpack boolean true', unpackTrue); 75 | }); 76 | 77 | group('Test Unpack Ints', () { 78 | test('Unpack Positive FixInt', unpackPositiveFixInt); 79 | test('Unpack Negative FixInt', unpackNegativeFixInt); 80 | test('Unpack Uint8', unpackUint8); 81 | test('Unpack Uint16', unpackUint16); 82 | test('Unpack Uint32', unpackUint32); 83 | test('Unpack Uint64', unpackUint64); 84 | test('Unpack Int8', unpackInt8); 85 | test('Unpack Int16', unpackInt16); 86 | test('Unpack Int32', unpackInt32); 87 | test('Unpack Int64', unpackInt64); 88 | }); 89 | 90 | group('Test Unpack Floats', () { 91 | test('Unpack Float32', unpackFloat32); 92 | test('Unpack Float64 (double)', unpackDouble); 93 | }); 94 | 95 | test('Unpack 5-character string', unpackString5); 96 | test('Unpack 22-character string', unpackString22); 97 | test('Unpack 256-character string', unpackString256); 98 | test('Unpack 70000-character string', unpackString70000); 99 | test('Unpack string array', unpackStringArray); 100 | test('Unpack int-to-string map', unpackIntToStringMap); 101 | 102 | group('Test Large Array and Map', () { 103 | test('Large Array', largeArray); 104 | test('Very Large Array', veryLargeArray); 105 | test('Large Map', largeMap); 106 | test('Very Large Map', veryLargeMap); 107 | }); 108 | 109 | group('Test Unpack Extensions', () { 110 | test('Unpack timestamp32', unpackTimestamp32); 111 | test('Unpack timestamp64', unpackTimestamp64); 112 | test('Unpack timestamp96', unpackTimestamp96); 113 | }); 114 | 115 | test('Test Unpack chunked', unpackChunked); 116 | } 117 | 118 | void largeArray() { 119 | final list = []; 120 | for (var i = 0; i < 16; ++i) { 121 | list.add('Item $i'); 122 | } 123 | 124 | final serialized = msgPack.encode(list); 125 | final deserialized = msgPack.decode(serialized) as List; 126 | expect(deserialized, list); 127 | } 128 | 129 | void veryLargeArray() { 130 | final list = []; 131 | for (var i = 0; i < 65536; ++i) { 132 | list.add('Item $i'); 133 | } 134 | 135 | final serialized = msgPack.encode(list); 136 | final deserialized = msgPack.decode(serialized) as List; 137 | expect(deserialized, list); 138 | } 139 | 140 | void largeMap() { 141 | final map = {}; 142 | for (var i = 0; i < 16; ++i) { 143 | map[i] = 'Item $i'; 144 | } 145 | final serialized = msgPack.encode(map); 146 | final deserialized = msgPack.decode(serialized) as Map; 147 | expect(deserialized, map); 148 | } 149 | 150 | void veryLargeMap() { 151 | final map = {}; 152 | for (var i = 0; i < 65536; ++i) { 153 | map[i] = 'Item $i'; 154 | } 155 | final serialized = msgPack.encode(map); 156 | final deserialized = msgPack.decode(serialized) as Map; 157 | expect(deserialized, map); 158 | } 159 | 160 | void packNull() { 161 | final List encoded = msgPack.encode(null); 162 | expect(encoded, orderedEquals([0xc0])); 163 | } 164 | 165 | void packFalse() { 166 | final List encoded = msgPack.encode(false); 167 | expect(encoded, orderedEquals([0xc2])); 168 | } 169 | 170 | void packTrue() { 171 | final List encoded = msgPack.encode(true); 172 | expect(encoded, orderedEquals([0xc3])); 173 | } 174 | 175 | void packPositiveFixInt() { 176 | final List encoded = msgPack.encode(1); 177 | expect(encoded, orderedEquals([1])); 178 | } 179 | 180 | void packFixedNegative() { 181 | final List encoded = msgPack.encode(-16); 182 | expect(encoded, orderedEquals([240])); 183 | } 184 | 185 | void packUint8() { 186 | final List encoded = msgPack.encode(128); 187 | expect(encoded, orderedEquals([204, 128])); 188 | } 189 | 190 | void packUint16() { 191 | final List encoded = msgPack.encode(32768); 192 | expect(encoded, orderedEquals([205, 128, 0])); 193 | } 194 | 195 | void packUint32() { 196 | final List encoded = msgPack.encode(2147483648); 197 | expect(encoded, orderedEquals([206, 128, 0, 0, 0])); 198 | } 199 | 200 | void packUint64() { 201 | final List encoded = msgPack.encode(uint64TestValue); 202 | expect(encoded, orderedEquals(uint64Packed)); 203 | } 204 | 205 | void packInt8() { 206 | final List encoded = msgPack.encode(-128); 207 | expect(encoded, orderedEquals([208, 128])); 208 | } 209 | 210 | void packInt16() { 211 | final List encoded = msgPack.encode(-32768); 212 | expect(encoded, orderedEquals([209, 128, 0])); 213 | } 214 | 215 | void packInt32() { 216 | final List encoded = msgPack.encode(-2147483648); 217 | expect(encoded, orderedEquals([210, 128, 0, 0, 0])); 218 | } 219 | 220 | void packInt64() { 221 | final List encoded = msgPack.encode(int64TestValue); 222 | expect(encoded, orderedEquals(int64Packed)); 223 | } 224 | 225 | void packFloat32() { 226 | final List encoded = msgPack.encode(const Float(3.14)); 227 | expect(encoded, orderedEquals([202, 64, 72, 245, 195])); 228 | } 229 | 230 | void packDouble() { 231 | final List encoded = msgPack.encode(3.14); 232 | expect( 233 | encoded, 234 | orderedEquals([0xcb, 0x40, 0x09, 0x1e, 0xb8, 0x51, 0xeb, 0x85, 0x1f]), 235 | ); 236 | } 237 | 238 | void packString5() { 239 | final List encoded = msgPack.encode('hello'); 240 | expect(encoded, orderedEquals([165, 104, 101, 108, 108, 111])); 241 | } 242 | 243 | void packString22() { 244 | final List encoded = msgPack.encode('hello there, everyone!'); 245 | expect( 246 | encoded, 247 | orderedEquals([ 248 | 182, 249 | 104, 250 | 101, 251 | 108, 252 | 108, 253 | 111, 254 | 32, 255 | 116, 256 | 104, 257 | 101, 258 | 114, 259 | 101, 260 | 44, 261 | 32, 262 | 101, 263 | 118, 264 | 101, 265 | 114, 266 | 121, 267 | 111, 268 | 110, 269 | 101, 270 | 33, 271 | ]), 272 | ); 273 | } 274 | 275 | void packString256() { 276 | final List encoded = msgPack.encode( 277 | // ignore: lines_longer_than_80_chars 278 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 279 | ); 280 | expect(encoded, hasLength(259)); 281 | expect(encoded.sublist(0, 3), orderedEquals([218, 1, 0])); 282 | expect(encoded.sublist(3, 259), everyElement(65)); 283 | } 284 | 285 | void packString70000() { 286 | final List encoded = msgPack.encode('A' * 70000); 287 | expect(encoded, hasLength(70005)); 288 | expect(encoded.sublist(0, 5), orderedEquals([219, 0x00, 0x01, 0x11, 0x70])); 289 | expect(encoded.sublist(5, 70005), everyElement(65)); 290 | } 291 | 292 | void packBin8() { 293 | final data = Uint8List.fromList(List.filled(32, 65)); 294 | final List encoded = msgPack.encode(data); 295 | expect(encoded.length, equals(34)); 296 | expect(encoded.getRange(0, 2), orderedEquals([0xc4, 32])); 297 | expect(encoded.getRange(2, encoded.length), orderedEquals(data)); 298 | } 299 | 300 | void packBin16() { 301 | final data = Uint8List.fromList(List.filled(256, 65)); 302 | final List encoded = msgPack.encode(data); 303 | expect(encoded.length, equals(256 + 3)); 304 | expect(encoded.getRange(0, 3), orderedEquals([0xc5, 1, 0])); 305 | expect(encoded.getRange(3, encoded.length), orderedEquals(data)); 306 | } 307 | 308 | void packBin32() { 309 | final data = Uint8List.fromList(List.filled(65536, 65)); 310 | final List encoded = msgPack.encode(data); 311 | expect(encoded.length, equals(65536 + 5)); 312 | expect(encoded.getRange(0, 5), orderedEquals([0xc6, 0, 1, 0, 0])); 313 | expect(encoded.getRange(5, encoded.length), orderedEquals(data)); 314 | } 315 | 316 | void packByteData() { 317 | final data = ByteData.view(Uint8List.fromList(List.filled(32, 65)).buffer); 318 | final List encoded = msgPack.encode(data); 319 | expect(encoded.length, equals(34)); 320 | expect(encoded.getRange(0, 2), orderedEquals([0xc4, 32])); 321 | expect( 322 | encoded.getRange(2, encoded.length), 323 | orderedEquals(data.buffer.asUint8List()), 324 | ); 325 | } 326 | 327 | void packStringArray() { 328 | final List encoded = msgPack.encode(['one', 'two', 'three']); 329 | expect( 330 | encoded, 331 | orderedEquals([ 332 | 147, 333 | 163, 334 | 111, 335 | 110, 336 | 101, 337 | 163, 338 | 116, 339 | 119, 340 | 111, 341 | 165, 342 | 116, 343 | 104, 344 | 114, 345 | 101, 346 | 101, 347 | ]), 348 | ); 349 | } 350 | 351 | void packIntToStringMap() { 352 | final List encoded = msgPack.encode({1: 'one', 2: 'two'}); 353 | expect( 354 | encoded, 355 | orderedEquals([130, 1, 163, 111, 110, 101, 2, 163, 116, 119, 111]), 356 | ); 357 | } 358 | 359 | void packTimestamp32() { 360 | final timestamp = MsgpackTimestamp(BigInt.from(1605623935)); 361 | final encoded = msgPack.encode(timestamp); 362 | expect( 363 | encoded, 364 | orderedEquals([ 365 | // header 366 | 0xd6, 0xFF, 367 | // seconds 368 | 0x5F, 0xB3, 0xE0, 0x7F, 369 | ]), 370 | ); 371 | } 372 | 373 | void packTimestamp64() { 374 | final timestamp = MsgpackTimestamp( 375 | BigInt.from(1605623935), 376 | BigInt.from(123456000), 377 | ); 378 | // seconds(34): __00 0101 1111 1011 0011 1110 0000 0111 1111 379 | // nanoseconds(30): 0001 1101 0110 1111 0010 1000 0000 00__ 380 | final encoded = msgPack.encode(timestamp); 381 | expect( 382 | encoded, 383 | orderedEquals([ 384 | // header 385 | 0xd7, 0xFF, 386 | // nanoseconds 387 | 0x1D, 0x6F, 0x28, 388 | // nanoseconds + seconds 389 | 0x00, 390 | // seconds 391 | 0x5F, 0xB3, 0xE0, 0x7F, 392 | ]), 393 | ); 394 | } 395 | 396 | void packTimestamp96() { 397 | final timestamp = MsgpackTimestamp( 398 | BigInt.from(-23198174465), 399 | BigInt.from(987655321), 400 | ); 401 | final encoded = msgPack.encode(timestamp); 402 | expect( 403 | encoded, 404 | orderedEquals([ 405 | // header 406 | 0xc7, 12, 0xFF, 407 | // nanoseconds 408 | 0x3A, 0xDE, 0x6C, 0x99, 409 | // seconds 410 | 0xFF, 0xFF, 0xFF, 0xFA, 0x99, 0x47, 0xF2, 0xFF, 411 | ]), 412 | ); 413 | } 414 | 415 | Future packChunked() async { 416 | final data = ['this is', 1, true, 'test.']; 417 | 418 | final encoded = 419 | await Stream.fromIterable( 420 | data, 421 | ).transform(msgPack.encoder).expand((b) => b).toList(); 422 | 423 | expect(encoded, [ 424 | // this is 425 | 0Xa7, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 426 | // 1 427 | 0x01, 428 | // true 429 | 0xc3, 430 | // test. 431 | 0xa5, 0x74, 0x65, 0x73, 0x74, 0x2e, 432 | ]); 433 | } 434 | 435 | // Test unpacking 436 | void unpackNull() { 437 | final data = Uint8List.fromList([0xc0]); 438 | final value = msgPack.decode(data); 439 | expect(value, isNull); 440 | } 441 | 442 | void unpackFalse() { 443 | final data = Uint8List.fromList([0xc2]); 444 | final value = msgPack.decode(data); 445 | expect(value, isFalse); 446 | } 447 | 448 | void unpackTrue() { 449 | final data = Uint8List.fromList([0xc3]); 450 | final value = msgPack.decode(data); 451 | expect(value, isTrue); 452 | } 453 | 454 | void unpackString5() { 455 | final data = Uint8List.fromList([165, 104, 101, 108, 108, 111]); 456 | final value = msgPack.decode(data); 457 | expect(value, isString); 458 | expect(value, equals('hello')); 459 | } 460 | 461 | void unpackString22() { 462 | final data = Uint8List.fromList([ 463 | 182, 464 | 104, 465 | 101, 466 | 108, 467 | 108, 468 | 111, 469 | 32, 470 | 116, 471 | 104, 472 | 101, 473 | 114, 474 | 101, 475 | 44, 476 | 32, 477 | 101, 478 | 118, 479 | 101, 480 | 114, 481 | 121, 482 | 111, 483 | 110, 484 | 101, 485 | 33, 486 | ]); 487 | final value = msgPack.decode(data); 488 | expect(value, isString); 489 | expect(value, equals('hello there, everyone!')); 490 | } 491 | 492 | void unpackPositiveFixInt() { 493 | final data = Uint8List.fromList([1]); 494 | final value = msgPack.decode(data); 495 | expect(value, isInt); 496 | expect(value, equals(1)); 497 | } 498 | 499 | void unpackNegativeFixInt() { 500 | final data = Uint8List.fromList([240]); 501 | final value = msgPack.decode(data); 502 | expect(value, isInt); 503 | expect(value, equals(-16)); 504 | } 505 | 506 | void unpackUint8() { 507 | final data = Uint8List.fromList([204, 128]); 508 | final value = msgPack.decode(data); 509 | expect(value, isInt); 510 | expect(value, equals(128)); 511 | } 512 | 513 | void unpackUint16() { 514 | final data = Uint8List.fromList([205, 128, 0]); 515 | final value = msgPack.decode(data); 516 | expect(value, isInt); 517 | expect(value, equals(32768)); 518 | } 519 | 520 | void unpackUint32() { 521 | final data = Uint8List.fromList([206, 128, 0, 0, 0]); 522 | final value = msgPack.decode(data); 523 | expect(value, isInt); 524 | expect(value, equals(2147483648)); 525 | } 526 | 527 | void unpackUint64() { 528 | // Dart 2 doesn't support true Uint64 without using BigInt 529 | final data = Uint8List.fromList(uint64Packed); 530 | final value = msgPack.decode(data); 531 | expect(value, isInt); 532 | expect(value, equals(uint64TestValue)); 533 | } 534 | 535 | void unpackInt8() { 536 | final data = Uint8List.fromList([208, 128]); 537 | final value = msgPack.decode(data); 538 | expect(value, isInt); 539 | expect(value, equals(-128)); 540 | } 541 | 542 | void unpackInt16() { 543 | final data = Uint8List.fromList([209, 128, 0]); 544 | final value = msgPack.decode(data); 545 | expect(value, isInt); 546 | expect(value, equals(-32768)); 547 | } 548 | 549 | void unpackInt32() { 550 | final data = Uint8List.fromList([210, 128, 0, 0, 0]); 551 | final value = msgPack.decode(data); 552 | expect(value, isInt); 553 | expect(value, equals(-2147483648)); 554 | } 555 | 556 | void unpackInt64() { 557 | final data = Uint8List.fromList(int64Packed); 558 | final value = msgPack.decode(data); 559 | expect(value, isInt); 560 | expect(value, equals(int64TestValue)); 561 | } 562 | 563 | void unpackFloat32() { 564 | final data = Uint8List.fromList([202, 64, 72, 245, 195]); 565 | final value = msgPack.decode(data); 566 | expect((value as double).toStringAsPrecision(3), equals('3.14')); 567 | } 568 | 569 | void unpackDouble() { 570 | final data = Uint8List.fromList([ 571 | 0xcb, 572 | 0x40, 573 | 0x09, 574 | 0x1e, 575 | 0xb8, 576 | 0x51, 577 | 0xeb, 578 | 0x85, 579 | 0x1f, 580 | ]); 581 | final value = msgPack.decode(data); 582 | expect(value, equals(3.14)); 583 | } 584 | 585 | void unpackString256() { 586 | final data = Uint8List.fromList([218, 1, 0, ...List.filled(256, 65)]); 587 | final value = msgPack.decode(data); 588 | expect(value, isString); 589 | expect( 590 | value, 591 | equals( 592 | // ignore: lines_longer_than_80_chars 593 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 594 | ), 595 | ); 596 | } 597 | 598 | void unpackString70000() { 599 | final data = Uint8List.fromList([ 600 | 219, 601 | 0x00, 602 | 0x01, 603 | 0x11, 604 | 0x70, 605 | ...List.filled(70000, 65), 606 | ]); 607 | final value = msgPack.decode(data); 608 | expect(value, isString); 609 | expect(value, hasLength(70000)); 610 | expect(value, equals('A' * 70000)); 611 | } 612 | 613 | void unpackStringArray() { 614 | final data = Uint8List.fromList([ 615 | 147, 616 | 163, 617 | 111, 618 | 110, 619 | 101, 620 | 163, 621 | 116, 622 | 119, 623 | 111, 624 | 165, 625 | 116, 626 | 104, 627 | 114, 628 | 101, 629 | 101, 630 | ]); 631 | final value = msgPack.decode(data); 632 | expect(value, isList); 633 | expect(value, orderedEquals(['one', 'two', 'three'])); 634 | } 635 | 636 | void unpackIntToStringMap() { 637 | final data = Uint8List.fromList([ 638 | 130, 639 | 1, 640 | 163, 641 | 111, 642 | 110, 643 | 101, 644 | 2, 645 | 163, 646 | 116, 647 | 119, 648 | 111, 649 | ]); 650 | final value = msgPack.decode(data); 651 | expect(value, isMap); 652 | value as Map; 653 | expect(value[1], equals('one')); 654 | expect(value[2], equals('two')); 655 | } 656 | 657 | void unpackTimestamp32() { 658 | final data = Uint8List.fromList([ 659 | // header 660 | 0xd6, 0xFF, 661 | // seconds 662 | 0x5F, 0xB3, 0xE0, 0x7F, 663 | ]); 664 | final value = msgPack.decode(data); 665 | 666 | expect(value, isA()); 667 | expect(value, MsgpackTimestamp(BigInt.from(1605623935))); 668 | } 669 | 670 | void unpackTimestamp64() { 671 | final data = Uint8List.fromList([ 672 | // header 673 | 0xd7, 0xFF, 674 | // nanoseconds 675 | 0x1D, 0x6F, 0x34, 676 | // nanoseconds + seconds 677 | 0x54, 678 | // seconds 679 | 0x5F, 0xB3, 0xE0, 0x7F, 680 | ]); 681 | final value = msgPack.decode(data); 682 | 683 | expect(value, isA()); 684 | expect( 685 | value, 686 | MsgpackTimestamp(BigInt.from(1605623935), BigInt.from(123456789)), 687 | ); 688 | } 689 | 690 | void unpackTimestamp96() { 691 | final data = Uint8List.fromList([ 692 | // header 693 | 0xc7, 12, 0xFF, 694 | // nanoseconds 695 | 0x3A, 0xDE, 0x6C, 0x99, 696 | // seconds 697 | 0xFF, 0xFF, 0xFF, 0xFA, 0x99, 0x47, 0xF2, 0xFF, 698 | ]); 699 | final value = msgPack.decode(data); 700 | 701 | expect(value, isA()); 702 | expect( 703 | value, 704 | MsgpackTimestamp(BigInt.from(-23198174465), BigInt.from(987655321)), 705 | ); 706 | } 707 | 708 | Future unpackChunked() async { 709 | final data = [ 710 | // this is 711 | [0Xa7, 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73], 712 | [ 713 | // 1 714 | 0x01, 715 | // true 716 | 0xc3, 717 | // 718 | 0xa5, 719 | ], 720 | // test. 721 | [0x74, 0x65], [0x73, 0x74, 0x2e], 722 | ]; 723 | 724 | final decoded = 725 | await Stream.fromIterable( 726 | data, 727 | ).map(Uint8List.fromList).transform(msgPack.decoder).toList(); 728 | 729 | expect(decoded, ['this is', 1, true, 'test.']); 730 | } 731 | --------------------------------------------------------------------------------