blob: 8050292174c6c8cc7d6aec6fd102e0f5524208ae [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.
package io.flutter.plugin.common;
import io.flutter.BuildConfig;
import io.flutter.Log;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* MessageCodec using the Flutter standard binary encoding.
*
* <p>This codec is guaranteed to be compatible with the corresponding <a
* href="https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html">StandardMessageCodec</a>
* on the Dart side. These parts of the Flutter SDK are evolved synchronously.
*
* <p>Supported messages are acyclic values of these forms:
*
* <ul>
* <li>null
* <li>Booleans
* <li>Bytes, Shorts, Integers, Longs
* <li>BigIntegers (see below)
* <li>Floats, Doubles
* <li>Strings
* <li>byte[], int[], long[], float[], double[]
* <li>Lists of supported values
* <li>Maps with supported keys and values
* </ul>
*
* <p>On the Dart side, these values are represented as follows:
*
* <ul>
* <li>null: null
* <li>Boolean: bool
* <li>Byte, Short, Integer, Long: int
* <li>Float, Double: double
* <li>String: String
* <li>byte[]: Uint8List
* <li>int[]: Int32List
* <li>long[]: Int64List
* <li>float[]: Float32List
* <li>double[]: Float64List
* <li>List: List
* <li>Map: Map
* </ul>
*
* <p>BigIntegers are represented in Dart as strings with the hexadecimal representation of the
* integer's value.
*
* <p>To extend the codec, overwrite the writeValue and readValueOfType methods.
*/
public class StandardMessageCodec implements MessageCodec<Object> {
private static final String TAG = "StandardMessageCodec#";
public static final StandardMessageCodec INSTANCE = new StandardMessageCodec();
@Override
public ByteBuffer encodeMessage(Object message) {
if (message == null) {
return null;
}
final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
writeValue(stream, message);
final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
buffer.put(stream.buffer(), 0, stream.size());
return buffer;
}
@Override
public Object decodeMessage(ByteBuffer message) {
if (message == null) {
return null;
}
message.order(ByteOrder.nativeOrder());
final Object value = readValue(message);
if (message.hasRemaining()) {
throw new IllegalArgumentException("Message corrupted");
}
return value;
}
private static final boolean LITTLE_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
private static final Charset UTF8 = Charset.forName("UTF8");
private static final byte NULL = 0;
private static final byte TRUE = 1;
private static final byte FALSE = 2;
private static final byte INT = 3;
private static final byte LONG = 4;
private static final byte BIGINT = 5;
private static final byte DOUBLE = 6;
private static final byte STRING = 7;
private static final byte BYTE_ARRAY = 8;
private static final byte INT_ARRAY = 9;
private static final byte LONG_ARRAY = 10;
private static final byte DOUBLE_ARRAY = 11;
private static final byte LIST = 12;
private static final byte MAP = 13;
private static final byte FLOAT_ARRAY = 14;
/**
* Writes an int representing a size to the specified stream. Uses an expanding code of 1 to 5
* bytes to optimize for small values.
*/
protected static final void writeSize(ByteArrayOutputStream stream, int value) {
if (BuildConfig.DEBUG && 0 > value) {
Log.e(TAG, "Attempted to write a negative size.");
}
if (value < 254) {
stream.write(value);
} else if (value <= 0xffff) {
stream.write(254);
writeChar(stream, value);
} else {
stream.write(255);
writeInt(stream, value);
}
}
/** Writes the least significant two bytes of the specified int to the specified stream. */
protected static final void writeChar(ByteArrayOutputStream stream, int value) {
if (LITTLE_ENDIAN) {
stream.write(value);
stream.write(value >>> 8);
} else {
stream.write(value >>> 8);
stream.write(value);
}
}
/** Writes the specified int as 4 bytes to the specified stream. */
protected static final void writeInt(ByteArrayOutputStream stream, int value) {
if (LITTLE_ENDIAN) {
stream.write(value);
stream.write(value >>> 8);
stream.write(value >>> 16);
stream.write(value >>> 24);
} else {
stream.write(value >>> 24);
stream.write(value >>> 16);
stream.write(value >>> 8);
stream.write(value);
}
}
/** Writes the specified long as 8 bytes to the specified stream. */
protected static final void writeLong(ByteArrayOutputStream stream, long value) {
if (LITTLE_ENDIAN) {
stream.write((byte) value);
stream.write((byte) (value >>> 8));
stream.write((byte) (value >>> 16));
stream.write((byte) (value >>> 24));
stream.write((byte) (value >>> 32));
stream.write((byte) (value >>> 40));
stream.write((byte) (value >>> 48));
stream.write((byte) (value >>> 56));
} else {
stream.write((byte) (value >>> 56));
stream.write((byte) (value >>> 48));
stream.write((byte) (value >>> 40));
stream.write((byte) (value >>> 32));
stream.write((byte) (value >>> 24));
stream.write((byte) (value >>> 16));
stream.write((byte) (value >>> 8));
stream.write((byte) value);
}
}
/** Writes the specified double as 4 bytes to the specified stream */
protected static final void writeFloat(ByteArrayOutputStream stream, float value) {
writeInt(stream, Float.floatToIntBits(value));
}
/** Writes the specified double as 8 bytes to the specified stream. */
protected static final void writeDouble(ByteArrayOutputStream stream, double value) {
writeLong(stream, Double.doubleToLongBits(value));
}
/** Writes the length and then the actual bytes of the specified array to the specified stream. */
protected static final void writeBytes(ByteArrayOutputStream stream, byte[] bytes) {
writeSize(stream, bytes.length);
stream.write(bytes, 0, bytes.length);
}
/**
* Writes a number of padding bytes to the specified stream to ensure that the next value is
* aligned to a whole multiple of the specified alignment. An example usage with alignment = 8 is
* to ensure doubles are word-aligned in the stream.
*/
protected static final void writeAlignment(ByteArrayOutputStream stream, int alignment) {
final int mod = stream.size() % alignment;
if (mod != 0) {
for (int i = 0; i < alignment - mod; i++) {
stream.write(0);
}
}
}
/**
* Writes a type discriminator byte and then a byte serialization of the specified value to the
* specified stream.
*
* <p>Subclasses can extend the codec by overriding this method, calling super for values that the
* extension does not handle.
*/
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value == null || value.equals(null)) {
stream.write(NULL);
} else if (value instanceof Boolean) {
stream.write(((Boolean) value).booleanValue() ? TRUE : FALSE);
} else if (value instanceof Number) {
if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
stream.write(INT);
writeInt(stream, ((Number) value).intValue());
} else if (value instanceof Long) {
stream.write(LONG);
writeLong(stream, (long) value);
} else if (value instanceof Float || value instanceof Double) {
stream.write(DOUBLE);
writeAlignment(stream, 8);
writeDouble(stream, ((Number) value).doubleValue());
} else if (value instanceof BigInteger) {
stream.write(BIGINT);
writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
} else {
throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());
}
} else if (value instanceof CharSequence) {
stream.write(STRING);
writeBytes(stream, value.toString().getBytes(UTF8));
} else if (value instanceof byte[]) {
stream.write(BYTE_ARRAY);
writeBytes(stream, (byte[]) value);
} else if (value instanceof int[]) {
stream.write(INT_ARRAY);
final int[] array = (int[]) value;
writeSize(stream, array.length);
writeAlignment(stream, 4);
for (final int n : array) {
writeInt(stream, n);
}
} else if (value instanceof long[]) {
stream.write(LONG_ARRAY);
final long[] array = (long[]) value;
writeSize(stream, array.length);
writeAlignment(stream, 8);
for (final long n : array) {
writeLong(stream, n);
}
} else if (value instanceof double[]) {
stream.write(DOUBLE_ARRAY);
final double[] array = (double[]) value;
writeSize(stream, array.length);
writeAlignment(stream, 8);
for (final double d : array) {
writeDouble(stream, d);
}
} else if (value instanceof List) {
stream.write(LIST);
final List<?> list = (List) value;
writeSize(stream, list.size());
for (final Object o : list) {
writeValue(stream, o);
}
} else if (value instanceof Map) {
stream.write(MAP);
final Map<?, ?> map = (Map) value;
writeSize(stream, map.size());
for (final Entry<?, ?> entry : map.entrySet()) {
writeValue(stream, entry.getKey());
writeValue(stream, entry.getValue());
}
} else if (value instanceof float[]) {
stream.write(FLOAT_ARRAY);
final float[] array = (float[]) value;
writeSize(stream, array.length);
writeAlignment(stream, 4);
for (final float f : array) {
writeFloat(stream, f);
}
} else {
throw new IllegalArgumentException(
"Unsupported value: '" + value + "' of type '" + value.getClass() + "'");
}
}
/** Reads an int representing a size as written by writeSize. */
protected static final int readSize(ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
throw new IllegalArgumentException("Message corrupted");
}
final int value = buffer.get() & 0xff;
if (value < 254) {
return value;
} else if (value == 254) {
return buffer.getChar();
} else {
return buffer.getInt();
}
}
/** Reads a byte array as written by writeBytes. */
protected static final byte[] readBytes(ByteBuffer buffer) {
final int length = readSize(buffer);
final byte[] bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
/** Reads alignment padding bytes as written by writeAlignment. */
protected static final void readAlignment(ByteBuffer buffer, int alignment) {
final int mod = buffer.position() % alignment;
if (mod != 0) {
buffer.position(buffer.position() + alignment - mod);
}
}
/** Reads a value as written by writeValue. */
protected final Object readValue(ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
throw new IllegalArgumentException("Message corrupted");
}
final byte type = buffer.get();
return readValueOfType(type, buffer);
}
/**
* Reads a value of the specified type.
*
* <p>Subclasses may extend the codec by overriding this method, calling super for types that the
* extension does not handle.
*/
protected Object readValueOfType(byte type, ByteBuffer buffer) {
final Object result;
switch (type) {
case NULL:
result = null;
break;
case TRUE:
result = true;
break;
case FALSE:
result = false;
break;
case INT:
result = buffer.getInt();
break;
case LONG:
result = buffer.getLong();
break;
case BIGINT:
{
final byte[] hex = readBytes(buffer);
result = new BigInteger(new String(hex, UTF8), 16);
break;
}
case DOUBLE:
readAlignment(buffer, 8);
result = buffer.getDouble();
break;
case STRING:
{
final byte[] bytes = readBytes(buffer);
result = new String(bytes, UTF8);
break;
}
case BYTE_ARRAY:
{
result = readBytes(buffer);
break;
}
case INT_ARRAY:
{
final int length = readSize(buffer);
final int[] array = new int[length];
readAlignment(buffer, 4);
buffer.asIntBuffer().get(array);
result = array;
buffer.position(buffer.position() + 4 * length);
break;
}
case LONG_ARRAY:
{
final int length = readSize(buffer);
final long[] array = new long[length];
readAlignment(buffer, 8);
buffer.asLongBuffer().get(array);
result = array;
buffer.position(buffer.position() + 8 * length);
break;
}
case DOUBLE_ARRAY:
{
final int length = readSize(buffer);
final double[] array = new double[length];
readAlignment(buffer, 8);
buffer.asDoubleBuffer().get(array);
result = array;
buffer.position(buffer.position() + 8 * length);
break;
}
case LIST:
{
final int size = readSize(buffer);
final List<Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(readValue(buffer));
}
result = list;
break;
}
case MAP:
{
final int size = readSize(buffer);
final Map<Object, Object> map = new HashMap<>();
for (int i = 0; i < size; i++) {
map.put(readValue(buffer), readValue(buffer));
}
result = map;
break;
}
case FLOAT_ARRAY:
{
final int length = readSize(buffer);
final float[] array = new float[length];
readAlignment(buffer, 4);
buffer.asFloatBuffer().get(array);
result = array;
buffer.position(buffer.position() + 4 * length);
break;
}
default:
throw new IllegalArgumentException("Message corrupted");
}
return result;
}
static final class ExposedByteArrayOutputStream extends ByteArrayOutputStream {
byte[] buffer() {
return buf;
}
}
}