blob: eccfa1ad0a87c91cdb1e667131793ac80569c04d [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.
/**
* @fileoverview Type decoder handles decoding types from a VOM stream by
* looking up by id.
*
* Definitions:
* Type / Defined Type - The standard VOM JavaScript type object representation.
* Partial Type - The type representation off the wire, identical to defined
* types but child types are described by type ids rather than actual complete
* type objects.
*
* Overview:
* Type decoders hold a cache of decoded types. Types are read off the wire in
* defineType() and then lazily converted from partial to defined types when
* they are needed in lookupType().
* @private
*/
module.exports = TypeDecoder;
/**
* Create a TypeDecoder.
* This holds the set of cached types and assists in decoding.
* @constructor
* @private
*/
function TypeDecoder() {
this._definedTypes = {};
// Partial types are similar to definedTypes but have type ids for child types
// rather than fully defined type structures.
this._partialTypes = {};
}
var kind = require('../vdl/kind.js');
var Type = require('../vdl/type.js');
var BootstrapTypes = require('./bootstrap-types.js');
var RawVomReader = require('./raw-vom-reader.js');
var unwrap = require('../vdl/type-util').unwrap;
var wiretype = require('../gen-vdl/v.io/v23/vom');
var promiseFor = require('../lib/async-helper').promiseFor;
var promiseWhile = require('../lib/async-helper').promiseWhile;
var endByte = unwrap(wiretype.WireCtrlEnd);
/**
* Looks up a type in the decoded types cache by id.
* @param {number} typeId The type id.
* @return {Type} The decoded type or undefined.
*/
TypeDecoder.prototype.lookupType = function(typeId) {
return this._lookupTypeImpl(typeId, true);
};
/**
* Looks up a type in the decoded types cache by id.
* @param {number} typeId The type id.
* @param {boolean} defineUndefined True if partial types that this method
* resolves to should be built. False otherwise.
* Partial types should only be built when this is called through lookupType so
* that they are built lazily.
* @return {Type} The decoded type or undefined.
*/
TypeDecoder.prototype._lookupTypeImpl = function(typeId, definePartialTypes) {
if (typeId < 0) {
throw new Error('invalid negative type id.');
}
var type = BootstrapTypes.idToType(typeId);
if (type !== undefined) {
return type;
}
if (definePartialTypes && this._partialTypes.hasOwnProperty(typeId)) {
this._tryBuildPartialType(typeId, this._partialTypes[typeId]);
}
return this._definedTypes[typeId];
};
/**
* Add a new type definition to the type cache.
* @param {number} typeId The id of the type.
* @param {Promise<Uint8Array>} The raw bytes that describe the type structure.
*/
TypeDecoder.prototype.defineType = function(typeId, messageBytes) {
if (typeId < 0) {
throw new Error('invalid negative type id ' + typeId + '.');
}
if (this._definedTypes[typeId] !== undefined ||
this._partialTypes[typeId] !== undefined) {
throw new Error('Cannot redefine type with id ' + typeId);
}
// Read the type in and add it to the partial type set.
var td = this;
return this._readPartialType(messageBytes).then(function(type) {
td._partialTypes[typeId] = type;
});
};
/**
* Flattens the type's dependencies into a typeId->(type, partial type) map.
* @private
* @throws {Error} If the type's dependencies are not available.
*/
TypeDecoder.prototype._flattenTypeDepGraph = function(typeId, typeDeps) {
// Already in map?
if (typeDeps[typeId] !== undefined) {
return;
}
// Already defined?
if (this._lookupTypeImpl(typeId, false) !== undefined) {
return;
}
// Allocate a type for the partial type.
if (!this._partialTypes.hasOwnProperty(typeId)) {
throw new Error('Type definition with ID ' + typeId +
' not received.');
}
var partialType = this._partialTypes[typeId];
typeDeps[typeId] = {
partialType: partialType,
type: new Type()
};
// Recurse.
if (partialType.namedTypeId !== undefined) {
this._flattenTypeDepGraph(partialType.namedTypeId, typeDeps);
}
if (partialType.keyTypeId !== undefined) {
this._flattenTypeDepGraph(partialType.keyTypeId, typeDeps);
}
if (partialType.elemTypeId !== undefined) {
this._flattenTypeDepGraph(partialType.elemTypeId, typeDeps);
}
var i;
if (partialType.typeIds !== undefined) {
for (i = 0; i < partialType.typeIds.length; i++) {
this._flattenTypeDepGraph(partialType.typeIds[i], typeDeps);
}
}
if (partialType.fields !== undefined) {
for (i = 0; i < partialType.fields.length; i++) {
this._flattenTypeDepGraph(partialType.fields[i].typeId, typeDeps);
}
}
};
/**
* Tries to build a partial type into a type.
* This has two steps:
* 1. Allocate type objects for all dependencies
* 2. Copy the type and replace the type id with the created types.
* 3. Copy named types and change the name.
*/
TypeDecoder.prototype._tryBuildPartialType = function(typeId) {
if (!this._partialTypes.hasOwnProperty(typeId)) {
throw new Error('Type definition with ID ' + typeId +
' not received.');
}
var partialType = this._partialTypes[typeId];
var flattenedTypes = {};
this._flattenTypeDepGraph(typeId, flattenedTypes);
var self = this;
var getType = function(id) {
var type = self._lookupTypeImpl(id, false);
if (type !== undefined) {
return type;
}
type = flattenedTypes[id].type;
if (type !== undefined) {
return type;
}
throw new Error('Type unexpectedly undefined.');
};
var id;
var type;
var i;
// All dependencies are ready. Build the type.
for (id in flattenedTypes) {
if (!flattenedTypes.hasOwnProperty(id)) {
continue;
}
partialType = flattenedTypes[id].partialType;
type = flattenedTypes[id].type;
if (partialType.namedTypeId !== undefined) {
// Handle named types in a second pass because it involves copying.
continue;
}
type.kind = partialType.kind;
if (partialType.name !== undefined) {
type.name = partialType.name;
}
if (partialType.labels !== undefined) {
type.labels = partialType.labels;
}
if (partialType.len !== undefined) {
type.len = partialType.len;
}
if (partialType.keyTypeId !== undefined) {
type.key = getType(partialType.keyTypeId);
}
if (partialType.elemTypeId !== undefined) {
type.elem = getType(partialType.elemTypeId);
}
if (partialType.typeIds !== undefined) {
type.types = new Array(partialType.typeIds.length);
for (i = 0; i < partialType.typeIds.length; i++) {
type.types[i] = getType(partialType.typeIds[i]);
}
}
if (partialType.fields !== undefined) {
type.fields = new Array(partialType.fields.length);
for (i = 0; i < partialType.fields.length; i++) {
var partialField = partialType.fields[i];
type.fields[i] = {
name: partialField.name,
type: getType(partialField.typeId)
};
}
}
}
// Now handle named types.
for (id in flattenedTypes) {
if (flattenedTypes.hasOwnProperty(id)) {
partialType = flattenedTypes[id].partialType;
type = flattenedTypes[id].type;
if (partialType.namedTypeId !== undefined) {
// Special case for named types.
var toCopy = getType(partialType.namedTypeId);
for (var fieldName in toCopy) {
if (toCopy.hasOwnProperty(fieldName)) {
type[fieldName] = toCopy[fieldName];
}
}
type.name = partialType.name;
}
}
}
// Now that the types are all prepared, make them immutable.
for (id in flattenedTypes) {
if (flattenedTypes.hasOwnProperty(id)) {
type = flattenedTypes[id].type;
// Make the type immutable, setting its _unique string too.
type.freeze();
// Define the type.
this._definedTypes[id] = type;
// Remove the type from the partial type set.
delete this._partialTypes[id];
}
}
};
/**
* Reads a type off of the wire.
* @param {RawVomReader} reader The reader with the data
* @param {module:vanadium.vdl.kind} kind The kind that is being read.
* @param {string} wireName The name of the type. This is used to generate
* error messages
* @param {object[]} indexMap An array of options specifying how to read the
* fields of the type object. The index in the array is the index in the wire
* structure for the wire type. Each object in the array should have a key
* field which is the name of the field in the wire struct and a fn field with
* a function that will be called with this set to reader and returns a promise
* with its value. For instance:<br>
* <pre>[{key: 'name', fn: reader.readString)}]</pre>
* <br>
* Means the value at index 0 will correspond to the name field and should
* be read by reader.readString
* @returns {Promise<object>} A promise with the constructed wire type as the
* result.
*/
TypeDecoder.prototype._readTypeHelper = function(
reader, kind, wireName, indexMap) {
var partialType = {
name: '',
};
if (kind) {
partialType.kind = kind;
}
function notEndByte() {
return reader.tryReadControlByte().then(function(b) {
if (b === endByte) {
return false;
}
if (b !== null) {
return Promise.reject('Unknown control byte ' + b);
}
return true;
});
}
function readField() {
var entry;
return reader.readUint().then(function(nextIndex) {
entry = indexMap[nextIndex];
if (!entry) {
throw Error('Unexpected index for ' + wireName + ': ' + nextIndex);
}
return entry.fn.bind(reader)();
}).then(function(val) {
partialType[entry.key] = val;
});
}
return promiseWhile(notEndByte, readField).then(function() {
return partialType;
});
};
TypeDecoder.prototype._readNamedType = function(reader) {
return this._readTypeHelper(reader, null, 'WireNamed', [
{key: 'name', fn: reader.readString },
{key: 'namedTypeId', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readEnumType = function(reader) {
var labels = [];
var i = 0;
return this._readTypeHelper(reader, kind.ENUM, 'WireEnum',[
{ key: 'name', fn: reader.readString },
{ key: 'labels', fn: readLabels },
]);
function readLabels() {
return reader.readUint().then(function(length) {
labels = new Array(length);
return reader.readString().then(handleLabel);
});
}
function handleLabel(s) {
labels[i] = s;
i++;
if (i < labels.length) {
return reader.readString().then(handleLabel);
}
return labels;
}
};
TypeDecoder.prototype._readArrayType = function(reader) {
return this._readTypeHelper(reader, kind.ARRAY, 'WireArray', [
{key: 'name', fn: reader.readString },
{key: 'elemTypeId', fn: reader.readUint },
{key: 'len', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readListType = function(reader) {
return this._readTypeHelper(reader, kind.LIST, 'WireList', [
{key: 'name', fn: reader.readString },
{key: 'elemTypeId', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readOptionalType = function(reader) {
return this._readTypeHelper(reader, kind.OPTIONAL, 'WireList', [
{key: 'name', fn: reader.readString },
{key: 'elemTypeId', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readSetType = function(reader) {
return this._readTypeHelper(reader, kind.SET, 'WireSet', [
{key: 'name', fn: reader.readString },
{key: 'keyTypeId', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readMapType = function(reader) {
return this._readTypeHelper(reader, kind.MAP, 'WireMap', [
{key: 'name', fn: reader.readString },
{key: 'keyTypeId', fn: reader.readUint },
{key: 'elemTypeId', fn: reader.readUint },
]);
};
TypeDecoder.prototype._readStructOrUnionType = function(reader, kind) {
var fields = [];
var i = 0;
var td = this;
return this._readTypeHelper(reader, kind, 'WireStruct', [
{key: 'name', fn: reader.readString },
{key: 'fields', fn: readFields },
]).then(function(res) {
res.fields = res.fields || [];
return res;
});
function readFields() {
return reader.readUint().then(function(numFields) {
fields = new Array(numFields);
return promiseFor(numFields, readField);
}).then(function() {
return fields;
});
}
function readField() {
return td._readTypeHelper(reader, null, 'WireField', [
{key: 'name', fn: reader.readString },
{key: 'typeId', fn: reader.readUint },
]).then(function(field) {
fields[i] = field;
i++;
});
}
};
/**
* Read the binary type description into a partial type description.
* @param {Uint8Array} messageBytes The binary type message.
* @return {PartialType} The type that was read.
*/
TypeDecoder.prototype._readPartialType = function(messageBytes) {
var reader = new RawVomReader(messageBytes);
var td = this;
return reader.readUint().then(function(unionId) {
switch (unionId) {
case BootstrapTypes.unionIds.NAMED_TYPE:
return td._readNamedType(reader);
case BootstrapTypes.unionIds.ENUM_TYPE:
return td._readEnumType(reader);
case BootstrapTypes.unionIds.ARRAY_TYPE:
return td._readArrayType(reader);
case BootstrapTypes.unionIds.LIST_TYPE:
return td._readListType(reader);
case BootstrapTypes.unionIds.SET_TYPE:
return td._readSetType(reader);
case BootstrapTypes.unionIds.MAP_TYPE:
return td._readMapType(reader);
case BootstrapTypes.unionIds.STRUCT_TYPE:
return td._readStructOrUnionType(reader, kind.STRUCT);
case BootstrapTypes.unionIds.UNION_TYPE:
return td._readStructOrUnionType(reader, kind.UNION);
case BootstrapTypes.unionIds.OPTIONAL_TYPE:
return td._readOptionalType(reader);
default:
throw new Error('Unknown wire type id ' + unionId);
}
});
};