'use strict' const md5hex = require('md5-hex') 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 encoder = require('./encoder') 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 pluginRegistry = require('./pluginRegistry') const bigIntValue = require('./primitiveValues/bigInt') 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') const recursorUtils = require('./recursorUtils') // 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 = 3 // 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, bigIntValue.tag, bigIntValue.deserialize], [0x02, booleanValue.tag, booleanValue.deserialize], [0x03, nullValue.tag, nullValue.deserialize], [0x04, numberValue.tag, numberValue.deserialize], [0x05, stringValue.tag, stringValue.deserialize], [0x06, symbolValue.tag, symbolValue.deserialize], [0x07, undefinedValue.tag, undefinedValue.deserialize], [0x08, objectValue.tag, objectValue.deserialize], [0x09, statsDescriptors.iterableTag, statsDescriptors.deserializeIterableStats], [0x0A, statsDescriptors.listTag, statsDescriptors.deserializeListStats], [0x0B, itemDescriptor.complexTag, itemDescriptor.deserializeComplex], [0x0C, itemDescriptor.primitiveTag, itemDescriptor.deserializePrimitive], [0x0D, statsDescriptors.propertyTag, statsDescriptors.deserializePropertyStats], [0x0E, propertyDescriptor.complexTag, propertyDescriptor.deserializeComplex], [0x0F, propertyDescriptor.primitiveTag, propertyDescriptor.deserializePrimitive], [0x10, pointerDescriptor.tag, pointerDescriptor.deserialize], [0x11, mapValue.tag, mapValue.deserialize], [0x12, mapEntryDescriptor.tag, mapEntryDescriptor.deserialize], [0x13, argumentsValue.tag, argumentsValue.deserialize], [0x14, arrayBufferValue.tag, arrayBufferValue.deserialize], [0x15, boxedValue.tag, boxedValue.deserialize], [0x16, dataViewValue.tag, dataViewValue.deserialize], [0x17, dateValue.tag, dateValue.deserialize], [0x18, errorValue.tag, errorValue.deserialize], [0x19, functionValue.tag, functionValue.deserialize], [0x1A, globalValue.tag, globalValue.deserialize], [0x1B, promiseValue.tag, promiseValue.deserialize], [0x1C, regexpValue.tag, regexpValue.deserialize], [0x1D, setValue.tag, setValue.deserialize], [0x1E, typedArrayValue.tag, typedArrayValue.deserialize], [0x1F, 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 { // eslint-disable-line unicorn/custom-error-definition 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(x => serializeState(x)) 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)) { return descriptorsByPointerIndex.get(descriptor.index) } if (typeof rootDescriptor.createRecursor === 'function') { // The descriptor we're pointing to may be elsewhere in the serialized // structure. Consume the entire structure and check again. recursorUtils.consumeDeep(rootDescriptor.createRecursor()) if (descriptorsByPointerIndex.has(descriptor.index)) { return descriptorsByPointerIndex.get(descriptor.index) } } throw new PointerLookupError(descriptor.index) } 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)) } } const rootDescriptor = deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer) return rootDescriptor } exports.deserialize = deserialize