| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This file is hand-formatted. |
| |
| // This file must not import `dart:ui`, directly or indirectly, as it is |
| // intended to function even in pure Dart server or CLI environments. |
| import 'dart:convert'; |
| import 'dart:typed_data'; |
| |
| import 'model.dart'; |
| |
| /// The first four bytes of a Remote Flutter Widgets binary data blob. |
| /// |
| /// This signature is automatically added by [encodeDataBlob] and is checked in |
| /// [decodeDataBlob]. |
| /// |
| /// See also: |
| /// |
| /// * [libraryBlobSignature], which is the signature for binary library blobs. |
| const List<int> dataBlobSignature = <int>[0xFE, 0x52, 0x57, 0x44]; |
| |
| /// The first four bytes of a Remote Flutter Widgets binary library blob. |
| /// |
| /// This signature is automatically added by [encodeLibraryBlob] and is checked |
| /// in [decodeLibraryBlob]. |
| /// |
| /// See also: |
| /// |
| /// * [dataBlobSignature], which is the signature for binary data blobs. |
| const List<int> libraryBlobSignature = <int>[0xFE, 0x52, 0x46, 0x57]; |
| |
| /// Encode data as a Remote Flutter Widgets binary data blob. |
| /// |
| /// See also: |
| /// |
| /// * [decodeDataBlob], which decodes this format. |
| /// * [encodeLibraryBlob], which uses a superset of this format to encode |
| /// Remote Flutter Widgets binary library blobs. |
| Uint8List encodeDataBlob(Object value) { |
| final _BlobEncoder encoder = _BlobEncoder(); |
| encoder.writeSignature(dataBlobSignature); |
| encoder.writeValue(value); |
| return encoder.bytes.toBytes(); |
| } |
| |
| /// Decode a Remote Flutter Widgets binary data blob. |
| /// |
| /// This data is usually used in conjunction with [DynamicContent]. |
| /// |
| /// This method supports a subset of the format supported by |
| /// [decodeLibraryBlob]; specifically, it reads a _value_ from that format |
| /// (rather than a _library_), and disallows values other than maps, lists, |
| /// ints, doubles, booleans, and strings. See [decodeLibraryBlob] for a |
| /// description of the format. |
| /// |
| /// The first four bytes of the file (in hex) are FE 52 57 44; see |
| /// [dataBlobSignature]. |
| /// |
| /// See also: |
| /// |
| /// * [encodeDataBlob], which encodes this format. |
| /// * [decodeLibraryBlob], which uses a superset of this format to decode |
| /// Remote Flutter Widgets binary library blobs. |
| /// * [parseDataFile], which parses the text variant of this format. |
| Object decodeDataBlob(Uint8List bytes) { |
| final _BlobDecoder decoder = _BlobDecoder(bytes.buffer.asByteData(bytes.offsetInBytes, bytes.lengthInBytes)); |
| decoder.expectSignature(dataBlobSignature); |
| final Object result = decoder.readValue(); |
| if (!decoder.finished) { |
| throw const FormatException('Unexpected trailing bytes after value.'); |
| } |
| return result; |
| } |
| |
| /// Encode data as a Remote Flutter Widgets binary library blob. |
| /// |
| /// See also: |
| /// |
| /// * [decodeLibraryBlob], which decodes this format. |
| /// * [encodeDataBlob], which uses a subset of this format to decode |
| /// Remote Flutter Widgets binary data blobs. |
| /// * [parseLibraryFile], which parses the text variant of this format. |
| Uint8List encodeLibraryBlob(RemoteWidgetLibrary value) { |
| final _BlobEncoder encoder = _BlobEncoder(); |
| encoder.writeSignature(libraryBlobSignature); |
| encoder.writeLibrary(value); |
| return encoder.bytes.toBytes(); |
| } |
| |
| /// Decode a Remote Flutter Widgets binary library blob. |
| /// |
| /// Remote widget libraries are usually used in conjunction with a [Runtime]. |
| /// |
| /// ## Format |
| /// |
| /// This format is a depth-first serialization of the in-memory data structures, |
| /// using a one-byte tag to identify types when necessary, and using 64 bit |
| /// integers to encode lengths when necessary. |
| /// |
| /// The first four bytes of the file (in hex) are FE 52 46 57; see |
| /// [libraryBlobSignature]. |
| /// |
| /// Primitives in this format are as follows: |
| /// |
| /// * Integers are encoded as little-endian two's complement 64 bit integers. |
| /// For example, the number 513 (0x0000000000000201) is encoded as a 0x01 |
| /// byte, a 0x02 byte, and six 0x00 bytes, in that order. |
| /// |
| /// * Doubles are encoded as little-endian IEEE binary64 numbers. |
| /// |
| /// * Strings are encoded as an integer length followed by that many UTF-8 |
| /// encoded bytes. |
| /// |
| /// For example, the string "Hello" would be encoded as: |
| /// |
| /// 05 00 00 00 00 00 00 00 48 65 6C 6C 6F |
| /// |
| /// * Lists are encoded as an integer length, followed by that many values |
| /// back to back. When lists are of specific types (e.g. lists of imports), |
| /// each value in the list is encoded directly (untagged lists); when the list |
| /// can have multiple types, each value is prefixed by a tag giving the type, |
| /// followed by the value (tagged lists). For example, a list of integers with |
| /// the values 1 and 2 in that order would be encoded as: |
| /// |
| /// 02 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |
| /// 02 00 00 00 00 00 00 00 |
| /// |
| /// A list of arbitrary values that happens to contain one string "Hello" |
| /// would be encoded as follows; 0x04 is the tag for "String" (the full list |
| /// of tags is described below): |
| /// |
| /// 01 00 00 00 00 00 00 00 04 05 00 00 00 00 00 00 |
| /// 00 48 65 6C 6C 6F |
| /// |
| /// A list of length zero is eight zero bytes with no additional payload. |
| /// |
| /// * Maps are encoded as an integer length, followed by key/value pairs. For |
| /// maps where all the keys are strings (e.g. when encoding a [DynamicMap]), |
| /// the keys are given without tags (an untagged map). For maps where the keys |
| /// are of arbitrary values, the keys are prefixed by a tag byte (a tagged |
| /// map; this is only used when encoding [Switch]es). The _values_ are always |
| /// prefixed by a tag byte (all maps are over values of arbitrary types). |
| /// |
| /// For example, the map `{ a: 15 }` (when the keys are known to always be |
| /// strings, so they are untagged) is encoded as follows (0x02 is the tag for |
| /// integers): |
| /// |
| /// 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |
| /// 61 02 0F 00 00 00 00 00 00 00 |
| /// |
| /// Objects are encoded as follows: |
| /// |
| /// * [RemoteWidgetLibrary] objects are encoded as an untagged list of |
| /// imports and an untagged list of widget declarations. |
| /// |
| /// * Imports are encoded as an untagged list of strings, each of which is |
| /// one of the subparts of the imported library name. For example, `import |
| /// a.b` is encoded as: |
| /// |
| /// 02 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |
| /// 61 01 00 00 00 00 00 00 00 62 |
| /// |
| /// * Widget declarations are encoded as a string giving the declaration name, |
| /// an untagged map for the initial state, and finally the value that |
| /// represents the root of the widget declaration ([WidgetDeclaration.root], |
| /// which is always either a [Switch] or a [ConstructorCall]). |
| /// |
| /// When the widget's initial state is null, it is encoded as an empty map. By |
| /// extension, this means no distinction is made between a "stateless" remote |
| /// widget and a "stateful" remote widget whose initial state is empty. (This |
| /// is reasonable since if the initial state is empty, no state can ever be |
| /// changed, so the widget is in fact _de facto_ stateless.) |
| /// |
| /// Values are encoded as a tag byte followed by their data, as follows: |
| /// |
| /// * Booleans are encoded as just a tag, with the tag being 0x00 for false and |
| /// 0x01 for true. |
| /// |
| /// * Integers have the tag 0x02, and are encoded as described above (two's |
| /// complement, little-endian, 64 bit). |
| /// |
| /// * Doubles have the tag 0x03, and are encoded as described above |
| /// (little-endian binary64). |
| /// |
| /// * Strings have the tag 0x04, and are encoded as described above (length |
| /// followed by UTF-8 bytes). |
| /// |
| /// * Lists ([DynamicList]) have the tag 0x05, are encoded as described above |
| /// (length followed by tagged values). (Lists of untagged values are never |
| /// found in a "value" context.) |
| /// |
| /// * Maps ([DynamicMap]) have the tag 0x07, are encoded as described above |
| /// (length followed by pairs of strings and tagged values). (Tagged maps, |
| /// i.e. those with tagged keys, are never found in a "value" context.) |
| /// |
| /// * Loops ([Loop]) have the tag 0x08. They are encoded as two tagged values, |
| /// the [Loop.input] and the [Loop.output]. |
| /// |
| /// * Constructor calls ([ConstructorCall]) have the tag 0x09. They are encoded |
| /// as a string for the [ConstructorCall.name] followed by an untagged map |
| /// describing the [ConstructorCall.arguments]. |
| /// |
| /// * Argument, data, and state references ([ArgsReference], [DataReference], |
| /// and [StateReference] respectively) have tags 0x0A, 0x0B, and 0x0D |
| /// respectively, and are encoded as tagged lists of strings or integers |
| /// giving the [Reference.parts] of the reference. |
| /// |
| /// * Loop references ([LoopReference]) have the tag 0x0C, and are encoded as an |
| /// integer giving the number of [Loop] objects between the reference and the |
| /// loop being referenced (this is similar to a De Bruijn index), followed by |
| /// a tagged list of strings or integers giving the [Reference.parts] of the |
| /// reference. |
| /// |
| /// * Switches ([Switch]) have the tag 0x0F. They are encoded as a tagged value |
| /// describing the control value ([Switch.input]), followed by a tagged map |
| /// for the various case values ([Switch.outputs]). The default case is |
| /// represented by a value with tag 0x10 (and no data). |
| /// |
| /// For example, this switch: |
| /// |
| /// ``` |
| /// switch (args.a) { |
| /// 0: 'z', |
| /// 1: 'o', |
| /// default: 'd', |
| /// } |
| /// ``` |
| /// |
| /// ...is encoded as follows (including the tag for the switch itself): |
| /// |
| /// 0F 0A 01 00 00 00 00 00 00 00 61 03 00 00 00 00 |
| /// 00 00 00 02 00 00 00 00 00 00 00 00 04 01 00 00 |
| /// 00 00 00 00 00 7A 02 01 00 00 00 00 00 00 00 04 |
| /// 01 00 00 00 00 00 00 00 6F 10 04 01 00 00 00 00 |
| /// 00 00 00 64 |
| /// |
| /// * Event handlers have the tag 0x0E, and are encoded as a string |
| /// ([EventHandler.eventName]) and an untagged map |
| /// ([EventHandler.eventArguments]). |
| /// |
| /// * State-setting handlers have the tag 0x11, and are encoded as a tagged list |
| /// of strings or integers giving the [Reference.parts] of the state reference |
| /// ([SetStateHandler.stateReference]), followed by the tagged value to which |
| /// to set that state entry ([SetStateHandler.value]). |
| /// |
| /// See also: |
| /// |
| /// * [encodeLibraryBlob], which encodes this format. |
| /// * [decodeDataBlob], which uses a subset of this format to decode |
| /// Remote Flutter Widgets binary data blobs. |
| /// * [parseDataFile], which parses the text variant of this format. |
| RemoteWidgetLibrary decodeLibraryBlob(Uint8List bytes) { |
| final _BlobDecoder decoder = _BlobDecoder(bytes.buffer.asByteData(bytes.offsetInBytes, bytes.lengthInBytes)); |
| decoder.expectSignature(libraryBlobSignature); |
| final RemoteWidgetLibrary result = decoder.readLibrary(); |
| if (!decoder.finished) { |
| throw const FormatException('Unexpected trailing bytes after constructors.'); |
| } |
| return result; |
| } |
| |
| // endianess used by this format |
| const Endian _blobEndian = Endian.little; |
| |
| // magic signatures |
| const int _msFalse = 0x00; |
| const int _msTrue = 0x01; |
| const int _msInt64 = 0x02; |
| const int _msBinary64 = 0x03; |
| const int _msString = 0x04; |
| const int _msList = 0x05; |
| const int _msMap = 0x07; |
| const int _msLoop = 0x08; |
| const int _msWidget = 0x09; |
| const int _msArgsReference = 0x0A; |
| const int _msDataReference = 0x0B; |
| const int _msLoopReference = 0x0C; |
| const int _msStateReference = 0x0D; |
| const int _msEvent = 0x0E; |
| const int _msSwitch = 0x0F; |
| const int _msDefault = 0x10; |
| const int _msSetState = 0x11; |
| |
| /// API for decoding Remote Flutter Widgets binary blobs. |
| /// |
| /// Binary data blobs can be decoded by using [readValue]. |
| /// |
| /// Binary library blobs can be decoded by using [readLibrary]. |
| /// |
| /// In either case, if [finished] returns false after parsing the root token, |
| /// then there is unexpected further data in the file. |
| class _BlobDecoder { |
| _BlobDecoder(this.bytes); |
| |
| final ByteData bytes; |
| |
| int _cursor = 0; |
| |
| bool get finished => _cursor >= bytes.lengthInBytes; |
| |
| void _advance(String context, int length) { |
| if (_cursor + length > bytes.lengthInBytes) { |
| throw FormatException('Could not read $context at offset $_cursor: unexpected end of file.'); |
| } |
| _cursor += length; |
| } |
| |
| int _readByte() { |
| final int byteOffset = _cursor; |
| _advance('byte', 1); |
| return bytes.getUint8(byteOffset); |
| } |
| |
| int _readInt64() { |
| final int byteOffset = _cursor; |
| _advance('int64', 8); |
| return bytes.getInt64(byteOffset, _blobEndian); |
| } |
| |
| double _readBinary64() { |
| final int byteOffset = _cursor; |
| _advance('binary64', 8); |
| return bytes.getFloat64(byteOffset, _blobEndian); |
| } |
| |
| String _readString() { |
| final int length = _readInt64(); |
| final int byteOffset = _cursor; |
| _advance('string', length); |
| return utf8.decode(bytes.buffer.asUint8List(bytes.offsetInBytes + byteOffset, length)); |
| } |
| |
| List<Object> _readPartList() { |
| return List<Object>.generate(_readInt64(), (int index) { |
| final int type = _readByte(); |
| switch (type) { |
| case _msString: |
| return _readString(); |
| case _msInt64: |
| return _readInt64(); |
| default: |
| throw FormatException('Invalid reference type 0x${type.toRadixString(16).toUpperCase().padLeft(2, "0")} while decoding blob.'); |
| } |
| }); |
| } |
| |
| Map<String, Object?>? _readMap(Object Function() readNode, { bool nullIfEmpty = false }) { |
| final int count = _readInt64(); |
| if (count == 0 && nullIfEmpty) { |
| return null; |
| } |
| return DynamicMap.fromEntries( |
| Iterable<MapEntry<String, Object>>.generate( |
| count, |
| (int index) => MapEntry<String, Object>( |
| _readString(), |
| readNode(), |
| ), |
| ), |
| ); |
| } |
| |
| Object? _readSwitchKey() { |
| final int type = _readByte(); |
| if (type == _msDefault) { |
| return null; |
| } |
| return _parseArgument(type); |
| } |
| |
| Switch _readSwitch() { |
| final Object value = _readArgument(); |
| final int count = _readInt64(); |
| final Map<Object?, Object> cases = Map<Object?, Object>.fromEntries( |
| Iterable<MapEntry<Object?, Object>>.generate( |
| count, |
| (int index) => MapEntry<Object?, Object>( |
| _readSwitchKey(), |
| _readArgument(), |
| ), |
| ), |
| ); |
| return Switch(value, cases); |
| } |
| |
| Object _parseValue(int type, Object Function() readNode) { |
| switch (type) { |
| case _msFalse: |
| return false; |
| case _msTrue: |
| return true; |
| case _msInt64: |
| return _readInt64(); |
| case _msBinary64: |
| return _readBinary64(); |
| case _msString: |
| return _readString(); |
| case _msList: |
| return DynamicList.generate(_readInt64(), (int index) => readNode()); |
| case _msMap: |
| return _readMap(readNode)!; |
| default: throw FormatException('Unrecognized data type 0x${type.toRadixString(16).toUpperCase().padLeft(2, "0")} while decoding blob.'); |
| } |
| } |
| |
| Object readValue() { |
| final int type = _readByte(); |
| return _parseValue(type, readValue); |
| } |
| |
| Object _parseArgument(int type) { |
| switch (type) { |
| case _msLoop: |
| return Loop(_readArgument(), _readArgument()); |
| case _msWidget: |
| return _readWidget(); |
| case _msArgsReference: |
| return ArgsReference(_readPartList()); |
| case _msDataReference: |
| return DataReference(_readPartList()); |
| case _msLoopReference: |
| return LoopReference(_readInt64(), _readPartList()); |
| case _msStateReference: |
| return StateReference(_readPartList()); |
| case _msEvent: |
| return EventHandler(_readString(), _readMap(_readArgument)!); |
| case _msSwitch: |
| return _readSwitch(); |
| case _msSetState: |
| return SetStateHandler(StateReference(_readPartList()), _readArgument()); |
| default: |
| return _parseValue(type, _readArgument); |
| } |
| } |
| |
| Object _readArgument() { |
| final int type = _readByte(); |
| return _parseArgument(type); |
| } |
| |
| ConstructorCall _readWidget() { |
| final String name = _readString(); |
| return ConstructorCall(name, _readMap(_readArgument)!); |
| } |
| |
| WidgetDeclaration _readDeclaration() { |
| final String name = _readString(); |
| final DynamicMap? initialState = _readMap(readValue, nullIfEmpty: true); |
| final int type = _readByte(); |
| final BlobNode root; |
| switch (type) { |
| case _msSwitch: |
| root = _readSwitch(); |
| break; |
| case _msWidget: |
| root = _readWidget(); |
| break; |
| default: |
| throw FormatException('Unrecognized data type 0x${type.toRadixString(16).toUpperCase().padLeft(2, "0")} while decoding widget declaration root.'); |
| } |
| return WidgetDeclaration(name, initialState, root); |
| } |
| |
| List<WidgetDeclaration> _readDeclarationList() { |
| return List<WidgetDeclaration>.generate(_readInt64(), (int index) => _readDeclaration()); |
| } |
| |
| Import _readImport() { |
| return Import(LibraryName(List<String>.generate(_readInt64(), (int index) => _readString()))); |
| } |
| |
| List<Import> _readImportList() { |
| return List<Import>.generate(_readInt64(), (int index) => _readImport()); |
| } |
| |
| RemoteWidgetLibrary readLibrary() { |
| return RemoteWidgetLibrary(_readImportList(), _readDeclarationList()); |
| } |
| |
| void expectSignature(List<int> signature) { |
| assert(signature.length == 4); |
| final List<int> bytes = <int>[]; |
| bool match = true; |
| for (final int byte in signature) { |
| final int read = _readByte(); |
| bytes.add(read); |
| if (read != byte) { |
| match = false; |
| } |
| } |
| if (!match) { |
| throw FormatException( |
| 'File signature mismatch. ' |
| 'Expected ${signature.map<String>((int byte) => byte.toRadixString(16).toUpperCase().padLeft(2, "0")).join(" ")} ' |
| 'but found ${bytes.map<String>((int byte) => byte.toRadixString(16).toUpperCase().padLeft(2, "0")).join(" ")}.' |
| ); |
| } |
| } |
| } |
| |
| /// API for encoding Remote Flutter Widgets binary blobs. |
| /// |
| /// Binary data blobs can be serialized using [writeValue]. |
| /// |
| /// Binary library blobs can be serialized using [writeLibrary]. |
| /// |
| /// The output is in [bytes], and can be cleared manually to reuse the [_BlobEncoder]. |
| class _BlobEncoder { |
| _BlobEncoder(); |
| |
| static final Uint8List _scratchOut = Uint8List(8); |
| static final ByteData _scratchIn = _scratchOut.buffer.asByteData(_scratchOut.offsetInBytes, _scratchOut.lengthInBytes); |
| |
| final BytesBuilder bytes = BytesBuilder(); // copying builder -- we repeatedly add _scratchOut after changing it |
| |
| void _writeInt64(int value) { |
| _scratchIn.setInt64(0, value, _blobEndian); |
| bytes.add(_scratchOut); |
| } |
| |
| void _writeString(String value) { |
| final Uint8List buffer = utf8.encode(value) as Uint8List; |
| _writeInt64(buffer.length); |
| bytes.add(buffer); |
| } |
| |
| void _writeMap(DynamicMap value, void Function(Object? value) recurse) { |
| _writeInt64(value.length); |
| value.forEach((String key, Object? value) { |
| _writeString(key); |
| recurse(value); |
| }); |
| } |
| |
| void _writePart(Object? value) { |
| if (value is int) { |
| bytes.addByte(_msInt64); |
| _writeInt64(value); |
| } else if (value is String) { |
| bytes.addByte(_msString); |
| _writeString(value); |
| } else { |
| throw StateError('Unexpected type ${value.runtimeType} while encoding blob.'); |
| } |
| } |
| |
| void _writeValue(Object? value, void Function(Object? value) recurse) { |
| if (value == false) { |
| bytes.addByte(_msFalse); |
| } else if (value == true) { |
| bytes.addByte(_msTrue); |
| } else if (value is double) { |
| bytes.addByte(_msBinary64); |
| _scratchIn.setFloat64(0, value, _blobEndian); |
| bytes.add(_scratchOut); |
| } else if (value is DynamicList) { |
| bytes.addByte(_msList); |
| _writeInt64(value.length); |
| value.forEach(recurse); |
| } else if (value is DynamicMap) { |
| bytes.addByte(_msMap); |
| _writeMap(value, recurse); |
| } else { |
| _writePart(value); |
| } |
| } |
| |
| void writeValue(Object? value) { |
| _writeValue(value, writeValue); |
| } |
| |
| void _writeArgument(Object? value) { |
| if (value is Loop) { |
| bytes.addByte(_msLoop); |
| _writeArgument(value.input); |
| _writeArgument(value.output); |
| } else if (value is ConstructorCall) { |
| bytes.addByte(_msWidget); |
| _writeString(value.name); |
| _writeMap(value.arguments, _writeArgument); |
| } else if (value is ArgsReference) { |
| bytes.addByte(_msArgsReference); |
| _writeInt64(value.parts.length); |
| value.parts.forEach(_writePart); |
| } else if (value is DataReference) { |
| bytes.addByte(_msDataReference); |
| _writeInt64(value.parts.length); |
| value.parts.forEach(_writePart); |
| } else if (value is LoopReference) { |
| bytes.addByte(_msLoopReference); |
| _writeInt64(value.loop); |
| _writeInt64(value.parts.length); |
| value.parts.forEach(_writePart); |
| } else if (value is StateReference) { |
| bytes.addByte(_msStateReference); |
| _writeInt64(value.parts.length); |
| value.parts.forEach(_writePart); |
| } else if (value is EventHandler) { |
| bytes.addByte(_msEvent); |
| _writeString(value.eventName); |
| _writeMap(value.eventArguments, _writeArgument); |
| } else if (value is Switch) { |
| bytes.addByte(_msSwitch); |
| _writeArgument(value.input); |
| _writeInt64(value.outputs.length); |
| value.outputs.forEach((Object? key, Object value) { |
| if (key == null) { |
| bytes.addByte(_msDefault); |
| } else { |
| _writeArgument(key); |
| } |
| _writeArgument(value); |
| }); |
| } else if (value is SetStateHandler) { |
| bytes.addByte(_msSetState); |
| final StateReference reference = value.stateReference as StateReference; |
| _writeInt64(reference.parts.length); |
| reference.parts.forEach(_writePart); |
| _writeArgument(value.value); |
| } else { |
| assert(value is! BlobNode); |
| _writeValue(value, _writeArgument); |
| } |
| } |
| |
| void _writeDeclarationList(List<WidgetDeclaration> value) { |
| _writeInt64(value.length); |
| for (final WidgetDeclaration declaration in value) { |
| _writeString(declaration.name); |
| if (declaration.initialState != null) { |
| _writeMap(declaration.initialState!, _writeArgument); |
| } else { |
| _writeInt64(0); |
| } |
| _writeArgument(declaration.root); |
| } |
| } |
| |
| void _writeImportList(List<Import> value) { |
| _writeInt64(value.length); |
| for (final Import import in value) { |
| _writeInt64(import.name.parts.length); |
| import.name.parts.forEach(_writeString); |
| } |
| } |
| |
| void writeLibrary(RemoteWidgetLibrary library) { |
| _writeImportList(library.imports); |
| _writeDeclarationList(library.widgets); |
| } |
| |
| void writeSignature(List<int> signature) { |
| assert(signature.length == 4); |
| bytes.add(signature); |
| } |
| } |