blob: f2094db406d6f4ea154f995cda10de5351d078c7 [file] [log] [blame]
// 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';
/// 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(<int>[0xFE, 0x52, 0x57, 0x44]);
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 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(<int>[0xFE, 0x52, 0x57, 0x44]);
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(<int>[0xFE, 0x52, 0x46, 0x57]);
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.
///
/// 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(<int>[0xFE, 0x52, 0x46, 0x57]);
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 _readDouble() {
final int byteOffset = _cursor;
_advance('double', 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 _readDouble();
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);
}
}