diff options
Diffstat (limited to 'node_modules/concordance/lib/serialize.js')
-rw-r--r-- | node_modules/concordance/lib/serialize.js | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/node_modules/concordance/lib/serialize.js b/node_modules/concordance/lib/serialize.js new file mode 100644 index 000000000..07f568415 --- /dev/null +++ b/node_modules/concordance/lib/serialize.js @@ -0,0 +1,339 @@ +'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 |