wallet-core/node_modules/concordance/lib/serialize.js
2017-08-14 05:02:09 +02:00

340 lines
11 KiB
JavaScript

'use strict'
const md5hex = require('md5-hex')
const encoder = require('./encoder')
const pluginRegistry = require('./pluginRegistry')
const argumentsValue = require('./complexValues/arguments')
const arrayBufferValue = require('./complexValues/arrayBuffer')
const boxedValue = require('./complexValues/boxed')
const dataViewValue = require('./complexValues/dataView')
const dateValue = require('./complexValues/date')
const errorValue = require('./complexValues/error')
const functionValue = require('./complexValues/function')
const globalValue = require('./complexValues/global')
const mapValue = require('./complexValues/map')
const objectValue = require('./complexValues/object')
const promiseValue = require('./complexValues/promise')
const regexpValue = require('./complexValues/regexp')
const setValue = require('./complexValues/set')
const typedArrayValue = require('./complexValues/typedArray')
const itemDescriptor = require('./metaDescriptors/item')
const mapEntryDescriptor = require('./metaDescriptors/mapEntry')
const pointerDescriptor = require('./metaDescriptors/pointer')
const propertyDescriptor = require('./metaDescriptors/property')
const statsDescriptors = require('./metaDescriptors/stats')
const booleanValue = require('./primitiveValues/boolean')
const nullValue = require('./primitiveValues/null')
const numberValue = require('./primitiveValues/number')
const stringValue = require('./primitiveValues/string')
const symbolValue = require('./primitiveValues/symbol')
const undefinedValue = require('./primitiveValues/undefined')
// Increment if encoding layout, descriptor IDs, or value types change. Previous
// Concordance versions will not be able to decode buffers generated by a newer
// version, so changing this value will require a major version bump of
// Concordance itself. The version is encoded as an unsigned 16 bit integer.
const VERSION = 2
// Adding or removing mappings or changing an index requires the version in
// encoder.js to be bumped, which necessitates a major version bump of
// Concordance itself. Indexes are hexadecimal to make reading the binary
// output easier.
const mappings = [
[0x01, booleanValue.tag, booleanValue.deserialize],
[0x02, nullValue.tag, nullValue.deserialize],
[0x03, numberValue.tag, numberValue.deserialize],
[0x04, stringValue.tag, stringValue.deserialize],
[0x05, symbolValue.tag, symbolValue.deserialize],
[0x06, undefinedValue.tag, undefinedValue.deserialize],
[0x07, objectValue.tag, objectValue.deserialize],
[0x08, statsDescriptors.iterableTag, statsDescriptors.deserializeIterableStats],
[0x09, statsDescriptors.listTag, statsDescriptors.deserializeListStats],
[0x0A, itemDescriptor.complexTag, itemDescriptor.deserializeComplex],
[0x0B, itemDescriptor.primitiveTag, itemDescriptor.deserializePrimitive],
[0x0C, statsDescriptors.propertyTag, statsDescriptors.deserializePropertyStats],
[0x0D, propertyDescriptor.complexTag, propertyDescriptor.deserializeComplex],
[0x0E, propertyDescriptor.primitiveTag, propertyDescriptor.deserializePrimitive],
[0x0F, pointerDescriptor.tag, pointerDescriptor.deserialize],
[0x10, mapValue.tag, mapValue.deserialize],
[0x11, mapEntryDescriptor.tag, mapEntryDescriptor.deserialize],
[0x12, argumentsValue.tag, argumentsValue.deserialize],
[0x13, arrayBufferValue.tag, arrayBufferValue.deserialize],
[0x14, boxedValue.tag, boxedValue.deserialize],
[0x15, dataViewValue.tag, dataViewValue.deserialize],
[0x16, dateValue.tag, dateValue.deserialize],
[0x17, errorValue.tag, errorValue.deserialize],
[0x18, functionValue.tag, functionValue.deserialize],
[0x19, globalValue.tag, globalValue.deserialize],
[0x1A, promiseValue.tag, promiseValue.deserialize],
[0x1B, regexpValue.tag, regexpValue.deserialize],
[0x1C, setValue.tag, setValue.deserialize],
[0x1D, typedArrayValue.tag, typedArrayValue.deserialize],
[0x1E, typedArrayValue.bytesTag, typedArrayValue.deserializeBytes]
]
const tag2id = new Map(mappings.map(mapping => [mapping[1], mapping[0]]))
const id2deserialize = new Map(mappings.map(mapping => [mapping[0], mapping[2]]))
class DescriptorSerializationError extends Error {
constructor (descriptor) {
super('Could not serialize descriptor')
this.name = 'DescriptorSerializationError'
this.descriptor = descriptor
}
}
class MissingPluginError extends Error {
constructor (pluginName) {
super(`Could not deserialize buffer: missing plugin ${JSON.stringify(pluginName)}`)
this.name = 'MissingPluginError'
this.pluginName = pluginName
}
}
class PointerLookupError extends Error {
constructor (index) {
super(`Could not deserialize buffer: pointer ${index} could not be resolved`)
this.name = 'PointerLookupError'
this.index = index
}
}
class UnsupportedPluginError extends Error {
constructor (pluginName, serializerVersion) {
super(`Could not deserialize buffer: plugin ${JSON.stringify(pluginName)} expects a different serialization`)
this.name = 'UnsupportedPluginError'
this.pluginName = pluginName
this.serializerVersion = serializerVersion
}
}
class UnsupportedVersion extends Error {
constructor (serializerVersion) {
super('Could not deserialize buffer: a different serialization was expected')
this.name = 'UnsupportedVersion'
this.serializerVersion = serializerVersion
}
}
function shallowSerializeDescriptor (descriptor, resolvePluginRef) {
if (!descriptor.serialize) return undefined
return serializeState(descriptor.serialize(), resolvePluginRef)
}
function serializeState (state, resolvePluginRef) {
if (Array.isArray(state)) return state.map(serializeState)
if (state && state.tag) {
let id, pluginIndex
if (tag2id.has(state.tag)) {
id = tag2id.get(state.tag)
pluginIndex = 0
} else {
const ref = resolvePluginRef(state.tag)
if (ref) {
id = ref.id
pluginIndex = ref.pluginIndex
}
}
if (id !== undefined) {
const serialized = [pluginIndex, id, shallowSerializeDescriptor(state, resolvePluginRef)]
serialized[encoder.descriptorSymbol] = true
return serialized
}
}
return state
}
function serialize (descriptor) {
const usedPlugins = new Map()
const resolvePluginRef = tag => {
const ref = pluginRegistry.resolveDescriptorRef(tag)
if (!ref) return null
if (!usedPlugins.has(ref.name)) {
// Start at 1, since 0 is reserved for Concordance's descriptors.
const index = usedPlugins.size + 1
usedPlugins.set(ref.name, Object.assign({index}, ref.serialization))
}
return {
id: ref.id,
pluginIndex: usedPlugins.get(ref.name).index
}
}
const seen = new Set()
const stack = []
let topIndex = -1
let rootRecord
do {
if (descriptor.isComplex === true) {
if (seen.has(descriptor.pointer)) {
descriptor = pointerDescriptor.describe(descriptor.pointer)
} else {
seen.add(descriptor.pointer)
}
}
let id
let pluginIndex = 0
if (tag2id.has(descriptor.tag)) {
id = tag2id.get(descriptor.tag)
} else {
const ref = resolvePluginRef(descriptor.tag)
if (!ref) throw new DescriptorSerializationError(descriptor)
id = ref.id
pluginIndex = ref.pluginIndex
}
const record = {
id,
pluginIndex,
children: [],
state: shallowSerializeDescriptor(descriptor, resolvePluginRef)
}
if (!rootRecord) {
rootRecord = record
} else {
stack[topIndex].children.push(record)
}
if (descriptor.createRecursor) {
stack.push({ recursor: descriptor.createRecursor(), children: record.children })
topIndex++
}
while (topIndex >= 0) {
descriptor = stack[topIndex].recursor()
if (descriptor === null) {
stack.pop()
topIndex--
} else {
break
}
}
} while (topIndex >= 0)
return encoder.encode(VERSION, rootRecord, usedPlugins)
}
exports.serialize = serialize
function deserializeState (state, getDescriptorDeserializer) {
if (state && state[encoder.descriptorSymbol] === true) {
return shallowDeserializeDescriptor(state, getDescriptorDeserializer)
}
return Array.isArray(state)
? state.map(item => deserializeState(item, getDescriptorDeserializer))
: state
}
function shallowDeserializeDescriptor (entry, getDescriptorDeserializer) {
const deserializeDescriptor = getDescriptorDeserializer(entry[0], entry[1])
return deserializeDescriptor(entry[2])
}
function deserializeRecord (record, getDescriptorDeserializer, buffer) {
const deserializeDescriptor = getDescriptorDeserializer(record.pluginIndex, record.id)
const state = deserializeState(record.state, getDescriptorDeserializer)
if (record.pointerAddresses.length === 0) {
return deserializeDescriptor(state)
}
const endIndex = record.pointerAddresses.length
let index = 0
const recursor = () => {
if (index === endIndex) return null
const recursorRecord = encoder.decodeRecord(buffer, record.pointerAddresses[index++])
return deserializeRecord(recursorRecord, getDescriptorDeserializer, buffer)
}
return deserializeDescriptor(state, recursor)
}
function buildPluginMap (buffer, options) {
const cache = options && options.deserializedPluginsCache
const cacheKey = md5hex(buffer)
if (cache && cache.has(cacheKey)) return cache.get(cacheKey)
const decodedPlugins = encoder.decodePlugins(buffer)
if (decodedPlugins.size === 0) {
const pluginMap = new Map()
if (cache) cache.set(cacheKey, pluginMap)
return pluginMap
}
const deserializerLookup = new Map()
if (Array.isArray(options && options.plugins)) {
for (const deserializer of pluginRegistry.getDeserializers(options.plugins)) {
deserializerLookup.set(deserializer.name, deserializer)
}
}
const pluginMap = new Map()
for (const index of decodedPlugins.keys()) {
const used = decodedPlugins.get(index)
const pluginName = used.name
const serializerVersion = used.serializerVersion
// TODO: Allow plugin author to encode a helpful message in its serialization
if (!deserializerLookup.has(pluginName)) {
throw new MissingPluginError(pluginName)
}
if (serializerVersion !== deserializerLookup.get(pluginName).serializerVersion) {
throw new UnsupportedPluginError(pluginName, serializerVersion)
}
pluginMap.set(index, deserializerLookup.get(pluginName).id2deserialize)
}
if (cache) cache.set(cacheKey, pluginMap)
return pluginMap
}
function deserialize (buffer, options) {
const version = encoder.extractVersion(buffer)
if (version !== VERSION) throw new UnsupportedVersion(version)
const decoded = encoder.decode(buffer)
const pluginMap = buildPluginMap(decoded.pluginBuffer, options)
const descriptorsByPointerIndex = new Map()
const mapPointerDescriptor = descriptor => {
if (descriptor.isPointer === true) {
if (!descriptorsByPointerIndex.has(descriptor.index)) throw new PointerLookupError(descriptor.index)
return descriptorsByPointerIndex.get(descriptor.index)
} else if (descriptor.isComplex === true) {
descriptorsByPointerIndex.set(descriptor.pointer, descriptor)
}
return descriptor
}
const getDescriptorDeserializer = (pluginIndex, id) => {
return (state, recursor) => {
const deserializeDescriptor = pluginIndex === 0
? id2deserialize.get(id)
: pluginMap.get(pluginIndex).get(id)
return mapPointerDescriptor(deserializeDescriptor(state, recursor))
}
}
return deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer)
}
exports.deserialize = deserialize