diff --git a/packages/flutter/lib/src/foundation/serialization.dart b/packages/flutter/lib/src/foundation/serialization.dart index a5dfb4affa..aea53d0059 100644 --- a/packages/flutter/lib/src/foundation/serialization.dart +++ b/packages/flutter/lib/src/foundation/serialization.dart @@ -14,16 +14,16 @@ import 'package:typed_data/typed_buffers.dart' show Uint8Buffer; /// The byte order of serialized data is [Endianness.BIG_ENDIAN]. /// The byte order of deserialized data is [Endianness.HOST_ENDIAN]. class WriteBuffer { - Uint8Buffer _buffer; - ByteData _eightBytes; - Uint8List _eightBytesAsList; - WriteBuffer() { _buffer = new Uint8Buffer(); _eightBytes = new ByteData(8); _eightBytesAsList = _eightBytes.buffer.asUint8List(); } + Uint8Buffer _buffer; + ByteData _eightBytes; + Uint8List _eightBytesAsList; + void putUint8(int byte) { _buffer.add(byte); } @@ -60,9 +60,8 @@ class WriteBuffer { if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) { _buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length)); } else { - for (final int value in list) { + for (final int value in list) putInt32(value); - } } } @@ -71,9 +70,8 @@ class WriteBuffer { if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) { _buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length)); } else { - for (final int value in list) { + for (final int value in list) putInt64(value); - } } } @@ -82,18 +80,16 @@ class WriteBuffer { if (Endianness.HOST_ENDIAN == Endianness.BIG_ENDIAN) { _buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length)); } else { - for (final double value in list) { + for (final double value in list) putFloat64(value); - } } } void _alignTo(int alignment) { final int mod = _buffer.length % alignment; if (mod != 0) { - for (int i = 0; i < alignment - mod; i++) { + for (int i = 0; i < alignment - mod; i++) _buffer.add(0); - } } } @@ -152,9 +148,8 @@ class ReadBuffer { list = data.buffer.asInt32List(data.offsetInBytes + position, length); } else { final ByteData invertedData = new ByteData(4 * length); - for (int i = 0; i < length; i++) { + for (int i = 0; i < length; i++) invertedData.setInt32(i * 4, data.getInt32(position + i * 4, Endianness.HOST_ENDIAN)); - } list = new Int32List.view(invertedData.buffer); } position += 4 * length; @@ -168,9 +163,8 @@ class ReadBuffer { list = data.buffer.asInt64List(data.offsetInBytes + position, length); } else { final ByteData invertedData = new ByteData(8 * length); - for (int i = 0; i < length; i++) { + for (int i = 0; i < length; i++) invertedData.setInt64(i * 8, data.getInt64(position + i * 8, Endianness.HOST_ENDIAN)); - } list = new Int64List.view(invertedData.buffer); } position += 8 * length; @@ -184,9 +178,8 @@ class ReadBuffer { list = data.buffer.asFloat64List(data.offsetInBytes + position, length); } else { final ByteData invertedData = new ByteData(8 * length); - for (int i = 0; i < length; i++) { + for (int i = 0; i < length; i++) invertedData.setFloat64(i * 8, data.getFloat64(position + i * 8, Endianness.HOST_ENDIAN)); - } list = new Float64List.view(invertedData.buffer); } position += 8 * length; @@ -195,9 +188,8 @@ class ReadBuffer { void _alignTo(int alignment) { final int mod = position % alignment; - if (mod != 0) { + if (mod != 0) position += alignment - mod; - } } bool get hasRemaining => position < data.lengthInBytes; diff --git a/packages/flutter/lib/src/services/message_codecs.dart b/packages/flutter/lib/src/services/message_codecs.dart index e7d640f3ca..68bb4c8cae 100644 --- a/packages/flutter/lib/src/services/message_codecs.dart +++ b/packages/flutter/lib/src/services/message_codecs.dart @@ -138,11 +138,13 @@ class StandardMessageCodec implements MessageCodec { // * The serialization of the value itself follows the type byte. // * Lengths and sizes of serialized parts are encoded using an expanding // format optimized for the common case of small non-negative integers: - // * values 0..<254 using one byte with that value; - // * values 254..<2^16 using three bytes, the first of which is 254, the - // next two the usual big-endian unsigned representation of the value; - // * values 2^16..<2^32 using five bytes, the first of which is 255, the - // next four the usual big-endian unsigned representation of the value. + // * values 0..253 inclusive using one byte with that value; + // * values 254..2^16 inclusive using three bytes, the first of which is + // 254, the next two the usual big-endian unsigned representation of the + // value; + // * values 2^16+1..2^32 inclusive using five bytes, the first of which is + // 255, the next four the usual big-endian unsigned representation of the + // value. // * null, true, and false have empty serialization; they are encoded directly // in the type byte (using _kNull, _kTrue, _kFalse) // * Integers representable in 32 bits are encoded using 4 bytes big-endian, @@ -208,19 +210,19 @@ class StandardMessageCodec implements MessageCodec { } static void _writeSize(WriteBuffer buffer, int value) { - assert(0 <= value && value < 0xffffffff); + assert(0 <= value && value <= 0xffffffff); if (value < 254) { buffer.putUint8(value); - } else if (value < 0xffff) { + } else if (value <= 0xffff) { buffer.putUint8(254); buffer.putUint8(value >> 8); - buffer.putUint8(value & 0xff); + buffer.putUint8(value); } else { buffer.putUint8(255); buffer.putUint8(value >> 24); - buffer.putUint8((value >> 16) & 0xff); - buffer.putUint8((value >> 8) & 0xff); - buffer.putUint8(value & 0xff); + buffer.putUint8(value >> 16); + buffer.putUint8(value >> 8); + buffer.putUint8(value); } } @@ -230,15 +232,13 @@ class StandardMessageCodec implements MessageCodec { } else if (value is bool) { buffer.putUint8(value ? _kTrue : _kFalse); } else if (value is int) { - if (-0x7fffffff <= value && value < 0x7fffffff) { + if (-0x7fffffff - 1 <= value && value <= 0x7fffffff) { buffer.putUint8(_kInt32); buffer.putInt32(value); - } - else if (-0x7fffffffffffffff <= value && value < 0x7fffffffffffffff) { + } else if (-0x7fffffffffffffff - 1 <= value && value <= 0x7fffffffffffffff) { buffer.putUint8(_kInt64); buffer.putInt64(value); - } - else { + } else { buffer.putUint8(_kLargeInt); final List hex = UTF8.encoder.convert(value.toRadixString(16)); _writeSize(buffer, hex.length); @@ -292,12 +292,12 @@ class StandardMessageCodec implements MessageCodec { return value; } else if (value == 254) { return (buffer.getUint8() << 8) - | buffer.getUint8(); + | buffer.getUint8(); } else { return (buffer.getUint8() << 24) - | (buffer.getUint8() << 16) - | (buffer.getUint8() << 8) - | buffer.getUint8(); + | (buffer.getUint8() << 16) + | (buffer.getUint8() << 8) + | buffer.getUint8(); } } diff --git a/packages/flutter/test/services/message_codecs_test.dart b/packages/flutter/test/services/message_codecs_test.dart new file mode 100644 index 0000000000..e288522564 --- /dev/null +++ b/packages/flutter/test/services/message_codecs_test.dart @@ -0,0 +1,254 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:test/test.dart'; + +void main() { + group('Binary codec', () { + const MessageCodec binary = const BinaryCodec(); + test('should encode and decode simple messages', () { + _checkEncodeDecode(binary, null); + _checkEncodeDecode(binary, new ByteData(0)); + _checkEncodeDecode(binary, new ByteData(4)..setInt32(0, -7)); + }); + }); + group('String codec', () { + const MessageCodec string = const StringCodec(); + test('should encode and decode simple messages', () { + _checkEncodeDecode(string, null); + _checkEncodeDecode(string, ''); + _checkEncodeDecode(string, 'hello'); + _checkEncodeDecode(string, 'special chars >\u263A\u{1F602}<'); + }); + }); + group('JSON message codec', () { + const MessageCodec json = const JSONMessageCodec(); + test('should encode and decode simple messages', () { + _checkEncodeDecode(json, null); + _checkEncodeDecode(json, true); + _checkEncodeDecode(json, false); + _checkEncodeDecode(json, 7); + _checkEncodeDecode(json, -7); + _checkEncodeDecode(json, 98742923489); + _checkEncodeDecode(json, -98742923489); + _checkEncodeDecode(json, 98740023429234899324932473298438); + _checkEncodeDecode(json, -98740023429234899324932473298438); + _checkEncodeDecode(json, 3.14); + _checkEncodeDecode(json, ''); + _checkEncodeDecode(json, 'hello'); + _checkEncodeDecode(json, 'special chars >\u263A\u{1F602}<'); + }); + test('should encode and decode composite message', () { + final List message = [ + null, + true, + false, + -707, + -7000000007, + -70000000000000000000000000000000000000000000000007, + -3.14, + '', + 'hello', + ['nested', []], + { 'a': 'nested', 'b': {} }, + 'world', + ]; + _checkEncodeDecode(json, message); + }); + }); + group('Standard message codec', () { + const MessageCodec standard = const StandardMessageCodec(); + test('should encode integers correctly at boundary cases', () { + _checkEncoding( + standard, + -0x7fffffff - 1, + [3, 0x80, 0x00, 0x00, 0x00], + ); + _checkEncoding( + standard, + -0x7fffffff - 2, + [4, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff], + ); + _checkEncoding( + standard, + 0x7fffffff, + [3, 0x7f, 0xff, 0xff, 0xff], + ); + _checkEncoding( + standard, + 0x7fffffff + 1, + [4, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00], + ); + _checkEncoding( + standard, + -0x7fffffffffffffff - 1, + [4, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + ); + _checkEncoding( + standard, + -0x7fffffffffffffff - 2, + [5, 17]..addAll('-8000000000000001'.codeUnits), + ); + _checkEncoding( + standard, + 0x7fffffffffffffff, + [4, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ); + _checkEncoding( + standard, + 0x7fffffffffffffff + 1, + [5, 16]..addAll('8000000000000000'.codeUnits), + ); + }); + test('should encode sizes correctly at boundary cases', () { + _checkEncoding( + standard, + new Uint8List(253), + [8, 253]..addAll(new List.filled(253, 0)), + ); + _checkEncoding( + standard, + new Uint8List(254), + [8, 254, 0, 254]..addAll(new List.filled(254, 0)), + ); + _checkEncoding( + standard, + new Uint8List(0xffff), + [8, 254, 0xff, 0xff]..addAll(new List.filled(0xffff, 0)), + ); + _checkEncoding( + standard, + new Uint8List(0xffff + 1), + [8, 255, 0, 1, 0, 0]..addAll(new List.filled(0xffff + 1, 0)), + ); + }); + test('should encode and decode simple messages', () { + _checkEncodeDecode(standard, null); + _checkEncodeDecode(standard, true); + _checkEncodeDecode(standard, false); + _checkEncodeDecode(standard, 7); + _checkEncodeDecode(standard, -7); + _checkEncodeDecode(standard, 98742923489); + _checkEncodeDecode(standard, -98742923489); + _checkEncodeDecode(standard, 98740023429234899324932473298438); + _checkEncodeDecode(standard, -98740023429234899324932473298438); + _checkEncodeDecode(standard, 3.14); + _checkEncodeDecode(standard, double.INFINITY); + _checkEncodeDecode(standard, double.NAN); + _checkEncodeDecode(standard, ''); + _checkEncodeDecode(standard, 'hello'); + _checkEncodeDecode(standard, 'special chars >\u263A\u{1F602}<'); + }); + test('should encode and decode composite message', () { + final List message = [ + null, + true, + false, + -707, + -7000000007, + -70000000000000000000000000000000000000000000000007, + -3.14, + '', + 'hello', + new Uint8List.fromList([0xBA, 0x5E, 0xBA, 0x11]), + new Int32List.fromList([-0x7fffffff - 1, 0, 0x7fffffff]), + null, // ensures the offset of the following list is unaligned. + new Int64List.fromList( + [-0x7fffffffffffffff - 1, 0, 0x7fffffffffffffff]), + null, // ensures the offset of the following list is unaligned. + new Float64List.fromList([ + double.NEGATIVE_INFINITY, + -double.MAX_FINITE, + -double.MIN_POSITIVE, + -0.0, + 0.0, + double.MIN_POSITIVE, + double.MAX_FINITE, + double.INFINITY, + double.NAN + ]), + ['nested', []], + { 'a': 'nested', null: {} }, + 'world', + ]; + _checkEncodeDecode(standard, message); + }); + }); +} + +void _checkEncoding(MessageCodec codec, T message, List expectedBytes) { + final ByteData encoded = codec.encodeMessage(message); + expect( + encoded.buffer.asUint8List(0, encoded.lengthInBytes), + orderedEquals(expectedBytes), + ); +} + +void _checkEncodeDecode(MessageCodec codec, T message) { + final ByteData encoded = codec.encodeMessage(message); + final T decoded = codec.decodeMessage(encoded); + if (message == null) { + expect(encoded, isNull); + expect(decoded, isNull); + } else { + expect(_deepEquals(message, decoded), isTrue); + final ByteData encodedAgain = codec.encodeMessage(decoded); + expect( + encodedAgain.buffer.asUint8List(), + orderedEquals(encoded.buffer.asUint8List()), + ); + } +} + +bool _deepEquals(dynamic valueA, dynamic valueB) { + if (valueA is TypedData) + return valueB is TypedData && _deepEqualsTypedData(valueA, valueB); + if (valueA is List) + return valueB is List && _deepEqualsList(valueA, valueB); + if (valueA is Map) + return valueB is Map && _deepEqualsMap(valueA, valueB); + if (valueA is double && valueA.isNaN) + return valueB is double && valueB.isNaN; + return valueA == valueB; +} + +bool _deepEqualsTypedData(TypedData valueA, TypedData valueB) { + if (valueA is ByteData) { + return valueB is ByteData + && _deepEqualsList( + valueA.buffer.asUint8List(), valueB.buffer.asUint8List()); + } + if (valueA is Uint8List) + return valueB is Uint8List && _deepEqualsList(valueA, valueB); + if (valueA is Int32List) + return valueB is Int32List && _deepEqualsList(valueA, valueB); + if (valueA is Int64List) + return valueB is Int64List && _deepEqualsList(valueA, valueB); + if (valueA is Float64List) + return valueB is Float64List && _deepEqualsList(valueA, valueB); + throw 'Unexpected typed data: $valueA'; +} + +bool _deepEqualsList(List valueA, List valueB) { + if (valueA.length != valueB.length) + return false; + for (int i = 0; i < valueA.length; i++) { + if (!_deepEquals(valueA[i], valueB[i])) + return false; + } + return true; +} + +bool _deepEqualsMap(Map valueA, Map valueB) { + if (valueA.length != valueB.length) + return false; + for (final dynamic key in valueA.keys) { + if (!valueB.containsKey(key) || !_deepEquals(valueA[key], valueB[key])) + return false; + } + return true; +} \ No newline at end of file