blob: 8dd29656520f2bf38563c597420589ac9c5896ac [file] [log] [blame]
// Copyright 2015 The Vanadium 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.v.v23.vom;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import io.v.v23.vdl.GeneratedFromVdl;
import io.v.v23.vdl.Kind;
import io.v.v23.vdl.NativeTypes;
import io.v.v23.vdl.Types;
import io.v.v23.vdl.VdlAny;
import io.v.v23.vdl.VdlArray;
import io.v.v23.vdl.VdlField;
import io.v.v23.vdl.VdlOptional;
import io.v.v23.vdl.VdlStruct;
import io.v.v23.vdl.VdlType;
import io.v.v23.vdl.VdlType.Builder;
import io.v.v23.vdl.VdlType.PendingType;
import io.v.v23.vdl.VdlTypeObject;
import io.v.v23.vdl.VdlUnion;
import io.v.v23.vdl.VdlValue;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* BinaryDecoder reads a VDL value from {@code InputStream} encoded in binary VOM format.
*/
// TODO(sjr): make sure this works for all unexported VDL types.
public class BinaryDecoder {
private final BufferedInputStream in;
private final Map<TypeId, VdlType> decodedTypes;
private final Map<TypeId, WireType> wireTypes;
private boolean binaryMagicByteRead;
public BinaryDecoder(InputStream in) {
this.in = new BufferedInputStream(in);
this.decodedTypes = new HashMap<TypeId, VdlType>();
this.wireTypes = new HashMap<TypeId, WireType>();
this.binaryMagicByteRead = false;
}
/**
* Decodes a VDL value. Returns an instance of provided {@code Type}.
*
* @param targetType the type of returned object
* @return the decoded value
* @throws IOException
* @throws ConversionException
*/
public Object decodeValue(Type targetType) throws IOException, ConversionException {
if (!binaryMagicByteRead) {
if ((byte) in.read() != BinaryUtil.BINARY_MAGIC_BYTE) {
throw new CorruptVomStreamException(
String.format("The input stream should start with byte %02x",
BinaryUtil.BINARY_MAGIC_BYTE));
}
binaryMagicByteRead = true;
}
VdlType actualType = decodeType();
assertTypesCompatible(actualType, targetType);
if (targetType == Object.class) {
try {
targetType = Types.getReflectTypeForVdl(actualType);
} catch (IllegalArgumentException e) {
throw new ConversionException(e);
}
}
return readValueMessage(actualType, targetType);
}
/**
* Decodes a VDL value.
* The decoder tries to match named VDL types with Java classes generated from VDL by
* translating VDL type name to Java class name, initializing class and calling
* {@code Types.getReflectTypeForVdl}. If the decoder fails to find a matching class for VDL
* type it will construct a general {@code VdlValue}. Prefer to use {@code decodeValue(Type)}
* over this method.
*
* @return the decoded value
* @throws IOException
* @throws ConversionException
*/
public Object decodeValue() throws IOException, ConversionException {
return decodeValue(Object.class);
}
private void assertTypesCompatible(VdlType actualType, Type targetType)
throws ConversionException {
if (targetType != Object.class && targetType != VdlValue.class && !TypeCompatibility.
compatible(actualType, Types.getVdlTypeFromReflect(targetType))) {
throw new ConversionException(actualType, targetType, "types are incompatible");
}
}
private Object readValueMessage(VdlType actualType, Type targetType) throws IOException,
ConversionException {
if (BinaryUtil.hasBinaryMsgLen(actualType)) {
// Do nothing with this information for now.
BinaryUtil.decodeUint(in);
}
return readValue(actualType, targetType);
}
private VdlType decodeType() throws IOException, ConversionException {
while (true) {
long typeId = BinaryUtil.decodeInt(in);
if (typeId == 0) {
throw new CorruptVomStreamException("Unexpected zero type ID");
} else if (typeId > 0) {
return getType(new TypeId(typeId));
} else {
WireType wireType = (WireType) readValueMessage(WireType.VDL_TYPE, WireType.class);
wireTypes.put(new TypeId(-typeId), wireType);
}
}
}
private VdlType lookupType(TypeId typeId) {
VdlType type = BootstrapType.getBootstrapType(typeId);
if (type != null) {
return type;
} else if (decodedTypes.containsKey(typeId)) {
return decodedTypes.get(typeId);
} else {
return null;
}
}
private VdlType getType(TypeId typeId) throws CorruptVomStreamException {
VdlType type = lookupType(typeId);
if (type != null) {
return type;
} else {
WireToVdlTypeBuilder builder = new WireToVdlTypeBuilder();
PendingType pendingType = builder.lookupOrBuildPending(typeId);
builder.build();
return pendingType.built();
}
}
private Object readValue(VdlType actualType, Type targetType)
throws IOException, ConversionException {
ConversionTarget target;
if (targetType == VdlValue.class) {
target = new ConversionTarget(actualType);
} else if (targetType == Object.class) {
// This can happen only inside VDL Any, as top-level type is constructed
// Outside of readValue().
try {
targetType = Types.getReflectTypeForVdl(actualType);
target = new ConversionTarget(targetType, actualType);
} catch (IllegalArgumentException e) {
target = new ConversionTarget(actualType);
}
} else {
target = new ConversionTarget(targetType);
}
// Solve any/optional case.
if (actualType.getKind() != Kind.ANY && actualType.getKind() != Kind.OPTIONAL) {
if (target.getKind() == Kind.ANY) {
return new VdlAny(actualType, (Serializable) readValue(actualType, Object.class));
} else if (target.getKind() == Kind.OPTIONAL) {
Type elemType = ReflectUtil.getElementType(target.getTargetType(), 0);
return VdlOptional.of((VdlValue) readValue(actualType, elemType));
}
}
// Convert native value.
NativeTypes.Converter converter = Types.getNativeTypeConverter(target.getTargetType());
if (converter != null) {
VdlValue value = (VdlValue) readValue(actualType, converter.getWireType());
return converter.nativeFromVdlValue(value);
}
switch (actualType.getKind()) {
case ANY:
return readVdlAny(target);
case ARRAY:
case LIST:
return readVdlArrayOrVdlList(actualType, target);
case BOOL:
return readVdlBool(target);
case BYTE:
return readVdlByte(target);
case COMPLEX64:
case COMPLEX128:
return readVdlComplex(target);
case ENUM:
return readVdlEnum(actualType, target);
case FLOAT32:
case FLOAT64:
return readVdlFloat(target);
case INT16:
case INT32:
case INT64:
return readVdlInt(target);
case MAP:
case SET:
return readVdlMapOrSet(actualType, target);
case STRUCT:
return readVdlStruct(actualType, target);
case UNION:
return readVdlUnion(actualType, target);
case OPTIONAL:
return readVdlOptional(actualType, target);
case STRING:
return readVdlString(target);
case TYPEOBJECT:
return readVdlTypeObject();
case UINT16:
case UINT32:
case UINT64:
return readVdlUint(target);
default:
throw new ConversionException(actualType, targetType);
}
}
private Object createNullValue(ConversionTarget target) throws ConversionException {
if (target.getKind() == Kind.ANY) {
return new VdlAny();
} else if (target.getKind() == Kind.OPTIONAL) {
return new VdlOptional<VdlValue>(target.getVdlType());
} else {
throw new ConversionException("Can't create a null value of " + target.getTargetType());
}
}
private Object readVdlAny(ConversionTarget target) throws IOException, ConversionException {
if (peekFlag() == Constants.WIRE_CTRL_NIL) {
ByteStreams.skipFully(in, 1);
return createNullValue(target);
}
VdlType actualType = getType(new TypeId(BinaryUtil.decodeUint(in)));
if (target.getKind() == Kind.ANY) {
return new VdlAny(actualType, (Serializable) readValue(actualType, Object.class));
} else {
Type targetType = target.getTargetType();
assertTypesCompatible(actualType, targetType);
return readValue(actualType, targetType);
}
}
private Object readVdlArrayOrVdlList(VdlType actualType, ConversionTarget target)
throws IOException, ConversionException {
int len;
if (actualType.getKind() == Kind.LIST) {
len = (int) BinaryUtil.decodeUint(in);
} else {
long uint = BinaryUtil.decodeUint(in);
if (uint != 0) {
throw new CorruptVomStreamException(
"Array length should be encoded as 0, but it is " + uint);
}
len = actualType.getLength();
}
Class<?> targetClass = target.getTargetClass();
if (!List.class.isAssignableFrom(targetClass)) {
if (BinaryUtil.isBytes(actualType) && targetClass.equals(byte[].class)) {
return BinaryUtil.decodeBytes(in, len);
} else if (!targetClass.isArray()) {
return ConvertUtil.convertFromBytes(BinaryUtil.decodeBytes(in, len), target);
}
}
Type elementType = ReflectUtil.getElementType(target.getTargetType(), 0);
if (targetClass.isArray() || VdlArray.class.isAssignableFrom(targetClass)) {
int targetLen = len;
if (target.getKind() == Kind.ARRAY) {
if (len > target.getVdlType().getLength()) {
throw new ConversionException(actualType, target.getTargetType(),
"target array is too short");
}
targetLen = target.getVdlType().getLength();
}
Class<?> elementClass = ReflectUtil.getRawClass(elementType);
Object array = Array.newInstance(elementClass, targetLen);
for (int i = 0; i < len; i++) {
ReflectUtil.setArrayValue(array, i, readValue(actualType.getElem(), elementType),
elementClass);
}
return ReflectUtil.createGeneric(target, array);
} else {
List<Object> list = new ArrayList<Object>();
for (int i = 0; i < len; i++) {
list.add(readValue(actualType.getElem(), elementType));
}
return ReflectUtil.createGeneric(target, list);
}
}
private Object readVdlBool(ConversionTarget target) throws IOException, ConversionException {
return ReflectUtil.createPrimitive(target, BinaryUtil.decodeBoolean(in), Boolean.TYPE);
}
private Object readVdlByte(ConversionTarget target) throws IOException, ConversionException {
return ConvertUtil.convertFromByte(BinaryUtil.decodeBytes(in, 1)[0], target);
}
private Object readVdlComplex(ConversionTarget target) throws IOException, ConversionException {
return ConvertUtil.convertFromComplex(BinaryUtil.decodeDouble(in),
BinaryUtil.decodeDouble(in), target);
}
private Object readVdlEnum(VdlType actualType, ConversionTarget target) throws IOException,
ConversionException {
int enumIndex = (int) BinaryUtil.decodeUint(in);
byte[] bytes = actualType.getLabels().get(enumIndex).getBytes(BinaryUtil.UTF8_CHARSET);
return ConvertUtil.convertFromBytes(bytes, target);
}
private Object readVdlFloat(ConversionTarget target) throws IOException, ConversionException {
return ConvertUtil.convertFromDouble(BinaryUtil.decodeDouble(in), target);
}
private Object readVdlInt(ConversionTarget target) throws IOException, ConversionException {
return ConvertUtil.convertFromInt(BinaryUtil.decodeInt(in), target);
}
private Type getMapElemOrStructFieldType(ConversionTarget target, Object key)
throws ConversionException {
Class<?> targetClass = target.getTargetClass();
if (target.getKind() == Kind.MAP) {
return ReflectUtil.getElementType(target.getTargetType(), 1);
} else if (target.getKind() == Kind.SET) {
return Boolean.class;
} else if (targetClass == VdlStruct.class) {
return VdlValue.class;
} else {
String fieldName = (String) key;
try {
Field field = targetClass.getDeclaredField(BinaryUtil.firstCharToLower(fieldName));
return field.getGenericType();
} catch (NoSuchFieldException e) {
// OK, we'll try to look at annotations.
}
for (Field field : targetClass.getDeclaredFields()) {
GeneratedFromVdl annotation = field.getAnnotation(GeneratedFromVdl.class);
if (annotation != null && annotation.name().equals(fieldName)) {
return field.getGenericType();
}
}
return Object.class;
}
}
@SuppressWarnings("unchecked")
private void setMapElemOrStructField(ConversionTarget target, Object data, Object key,
Object elem, Type elemType) throws ConversionException {
if (target.getKind() == Kind.MAP) {
((Map<Object, Object>) data).put(key, elem);
} else if (target.getKind() == Kind.SET) {
if ((Boolean) elem) {
((Set<Object>) data).add(key);
}
} else if (data instanceof VdlStruct) {
((VdlStruct) data).assignField((String) key, (VdlValue) elem);
} else {
if (elemType == Object.class) {
// no such field, just skip it
return;
}
try {
Field f = data.getClass().getDeclaredField(
BinaryUtil.firstCharToLower((String) key));
f.setAccessible(true);
f.set(data, elem);
} catch (Exception e) {
throw new ConversionException("Can't set field " + key + " to " + elem + " of "
+ target.getTargetType(), e);
}
}
}
private Object createMapOrSetOrStruct(ConversionTarget target) throws ConversionException {
if (target.getKind() == Kind.MAP) {
return ReflectUtil.createGeneric(target, new HashMap<Object, Object>());
} else if (target.getKind() == Kind.SET) {
return ReflectUtil.createGeneric(target, new HashSet<Object>());
} else {
return ReflectUtil.createStruct(target);
}
}
private Type getTargetKeyType(ConversionTarget target) throws ConversionException {
if (target.getKind() == Kind.MAP || target.getKind() == Kind.SET) {
return ReflectUtil.getElementType(target.getTargetType(), 0);
} else {
return String.class;
}
}
private Object readVdlMapOrSet(VdlType actualType, ConversionTarget target)
throws IOException, ConversionException {
Object data = createMapOrSetOrStruct(target);
Type targetKeyType = getTargetKeyType(target);
int len = (int) BinaryUtil.decodeUint(in);
for (int i = 0; i < len; i++) {
Object key = readValue(actualType.getKey(), targetKeyType);
Type targetElemType = getMapElemOrStructFieldType(target, key);
Object elem;
if (actualType.getKind() == Kind.SET) {
if (targetElemType == VdlAny.class) {
elem = new VdlAny(Boolean.class, true);
} else {
elem = ReflectUtil.createPrimitive(new ConversionTarget(targetElemType),
true, Boolean.TYPE);
}
} else {
elem = readValue(actualType.getElem(), targetElemType);
}
setMapElemOrStructField(target, data, key, elem, targetElemType);
}
return data;
}
private Object readVdlStruct(VdlType actualType, ConversionTarget target)
throws IOException, ConversionException {
Object data = createMapOrSetOrStruct(target);
Type targetKeyType = getTargetKeyType(target);
boolean[] seen = new boolean[actualType.getFields().size()];
Arrays.fill(seen, false);
while (true) {
if (peekFlag() == Constants.WIRE_CTRL_END) {
ByteStreams.skipFully(in, 1);
break;
}
int index = (int) BinaryUtil.decodeUint(in);
seen[index] = true;
VdlField field = actualType.getFields().get(index);
Type targetElemType = getMapElemOrStructFieldType(target, field.getName());
Object key = ConvertUtil.convertFromBytes(BinaryUtil.getBytes(field.getName()),
new ConversionTarget(targetKeyType));
Object elem = readValue(field.getType(), targetElemType);
setMapElemOrStructField(target, data, key, elem, targetElemType);
}
// Now we need to fill zero values of struct if target is a map.
if (target.getKind() != Kind.MAP) {
return data;
}
for (int i = 0; i < actualType.getFields().size(); i++) {
if (seen[i]) {
continue;
}
VdlField field = actualType.getFields().get(i);
Type elemType = getMapElemOrStructFieldType(target, field.getName());
Object key = ConvertUtil.convertFromBytes(BinaryUtil.getBytes(field.getName()),
new ConversionTarget(targetKeyType));
VdlType elemVdlType = target.getVdlType().getElem();
Object elem;
// All user-defined types and java primitives have a default constructor that returns
// a zero value. For lists, maps and sets it's OK to return a zero vdl.Value because
// it will be an empty list, map or set. In other cases we need to return a zero
// vdl.Value.
if (elemType instanceof Class
&& ((Class<?>) elemType).getSuperclass() != VdlValue.class) {
try {
elem = ((Class<?>) elemType).newInstance();
} catch (Exception e) {
throw new ConversionException(field.getType(), elemType);
}
} else {
elem = VdlValue.zeroValue(elemVdlType);
}
setMapElemOrStructField(target, data, key, elem, elemType);
}
return data;
}
private Object readVdlUnion(VdlType actualType, ConversionTarget target) throws IOException,
ConversionException {
int index = (int) BinaryUtil.decodeUint(in);
if (index < 0 || index >= actualType.getFields().size()) {
throw new CorruptVomStreamException("Union index " + index + " is out of range " + 1 +
"..." + actualType.getFields().size());
}
VdlField actualField = actualType.getFields().get(index);
VdlType actualElemType = actualField.getType();
// Solve vdl.Value case.
if (target.getTargetClass() == VdlUnion.class) {
return new VdlUnion(actualType, index, actualElemType,
readValue(actualElemType, Object.class));
}
Class<?> targetClass = target.getTargetClass();
// This can happen if targetClass is NamedUnion.A.
if (targetClass.getSuperclass() != VdlUnion.class) {
targetClass = targetClass.getSuperclass();
}
// Look-up field class in target.
Class<?> fieldClass = null;
for (Class<?> klass : targetClass.getDeclaredClasses()) {
if (klass.getName().equals(targetClass.getName() + "$" + actualField.getName())) {
fieldClass = klass;
break;
}
}
if (fieldClass == null) {
throw new ConversionException(actualType, target.getTargetType());
}
try {
Type elemType = fieldClass.getDeclaredField("elem").getGenericType();
return fieldClass.getConstructor(ReflectUtil.getRawClass(elemType)).newInstance(
readValue(actualElemType, elemType));
} catch (Exception e) {
throw new ConversionException(actualType, target.getTargetType(), e);
}
}
private Object readVdlOptional(VdlType actualType, ConversionTarget target) throws IOException,
ConversionException {
if (peekFlag() == Constants.WIRE_CTRL_NIL) {
ByteStreams.skipFully(in, 1);
return createNullValue(target);
} else {
Type type = target.getTargetType();
if (target.getKind() == Kind.OPTIONAL) {
type = ReflectUtil.getElementType(target.getTargetType(), 0);
return VdlOptional.of((VdlValue) readValue(actualType.getElem(), type));
} else {
return readValue(actualType.getElem(), type);
}
}
}
private Object readVdlString(ConversionTarget target) throws IOException, ConversionException {
int len = (int) BinaryUtil.decodeUint(in);
byte[] bytes = BinaryUtil.decodeBytes(in, len);
return ConvertUtil.convertFromBytes(bytes, target);
}
private Object readVdlUint(ConversionTarget target) throws IOException, ConversionException {
return ConvertUtil.convertFromUint(BinaryUtil.decodeUint(in), target);
}
private Object readVdlTypeObject() throws IOException {
return new VdlTypeObject(getType(new TypeId(BinaryUtil.decodeUint(in))));
}
private byte peekFlag() throws IOException {
in.mark(1);
byte flag = (byte) in.read();
in.reset();
return flag;
}
/**
* Builds VdlType from wire type.
*/
private final class WireToVdlTypeBuilder {
private final Builder builder;
private final Map<TypeId, PendingType> pendingTypes;
public WireToVdlTypeBuilder() {
builder = new Builder();
pendingTypes = new HashMap<TypeId, PendingType>();
}
public void build() {
builder.build();
for (Map.Entry<TypeId, PendingType> entry : pendingTypes.entrySet()) {
VdlType vdlType = entry.getValue().built();
if (!Strings.isNullOrEmpty(vdlType.getName())) {
Types.loadClassForVdlName(vdlType.getName());
}
BinaryDecoder.this.decodedTypes.put(entry.getKey(), vdlType);
}
}
public PendingType lookupOrBuildPending(TypeId typeId) throws CorruptVomStreamException {
PendingType vdlType = lookupType(typeId);
if (vdlType != null) {
return vdlType;
}
return buildPendingType(typeId);
}
private PendingType lookupType(TypeId typeId) {
VdlType type = BinaryDecoder.this.lookupType(typeId);
if (type != null) {
return builder.builtPendingFromType(type);
} else if (pendingTypes.containsKey(typeId)) {
return pendingTypes.get(typeId);
}
return null;
}
private PendingType buildPendingType(TypeId typeId) throws CorruptVomStreamException {
WireType wireType = BinaryDecoder.this.wireTypes.get(typeId);
if (wireType == null) {
throw new CorruptVomStreamException("Unknown wire type " + typeId);
}
PendingType pending = builder.newPending();
pendingTypes.put(typeId, pending);
switch (wireType.getIndex()) {
// The mapping is defined in wireType.vdl and is not going to change.
case 0: // "NameT"
WireNamed wireNamed = (WireNamed) wireType.getElem();
return pending.setName(wireNamed.getName())
.assignBase(lookupOrBuildPending(wireNamed.getBase()));
case 1: // "EnumT"
WireEnum wireEnum = (WireEnum) wireType.getElem();
pending.setName(wireEnum.getName()).setKind(Kind.ENUM);
for (String label : wireEnum.getLabels()) {
pending.addLabel(label);
}
return pending;
case 2: // "ArrayT"
WireArray wireArray = (WireArray) wireType.getElem();
return pending.setName(wireArray.getName()).setKind(Kind.ARRAY)
.setLength((int) wireArray.getLen().getValue())
.setElem(lookupOrBuildPending(wireArray.getElem()));
case 3: // "ListT"
WireList wireList = (WireList) wireType.getElem();
return pending.setName(wireList.getName()).setKind(Kind.LIST)
.setElem(lookupOrBuildPending(wireList.getElem()));
case 4: // "SetT"
WireSet wireSet = (WireSet) wireType.getElem();
return pending.setName(wireSet.getName()).setKind(Kind.SET)
.setKey(lookupOrBuildPending(wireSet.getKey()));
case 5: // "MapT"
WireMap wireMap = (WireMap) wireType.getElem();
return pending.setName(wireMap.getName()).setKind(Kind.MAP)
.setKey(lookupOrBuildPending(wireMap.getKey()))
.setElem(lookupOrBuildPending(wireMap.getElem()));
case 6: // "StructT"
WireStruct wireStruct = (WireStruct) wireType.getElem();
pending.setName(wireStruct.getName()).setKind(Kind.STRUCT);
for (WireField field : wireStruct.getFields()) {
pending.addField(field.getName(), lookupOrBuildPending(field.getType()));
}
return pending;
case 7: // "UnionT"
WireUnion wireUnion = (WireUnion) wireType.getElem();
pending.setName(wireUnion.getName()).setKind(Kind.UNION);
for (WireField field : wireUnion.getFields()) {
pending.addField(field.getName(), lookupOrBuildPending(field.getType()));
}
return pending;
case 8: // "OptionalT"
WireOptional wireOptional = (WireOptional) wireType.getElem();
return pending.setName(wireOptional.getName()).setKind(Kind.OPTIONAL)
.setElem(lookupOrBuildPending(wireOptional.getElem()));
default:
throw new CorruptVomStreamException("Unknown wire type: " + wireType.vdlType());
}
}
}
}