340 lines
11 KiB
JavaScript
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
|