From 2182889ca0795d48a71e8a658e999099ba9af451 Mon Sep 17 00:00:00 2001 From: ninevra Date: Sat, 13 Feb 2021 07:28:06 -0800 Subject: [PATCH] Fully deserialize as necessary to look up pointers Fixes #66. Co-authored-by: Mark Wubben --- lib/recursorUtils.js | 16 ++++++++++++++++ lib/serialize.js | 29 +++++++++++++++++++++++++---- test/diff.js | 22 ++++++++++++++++++++++ test/format.js | 22 ++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/lib/recursorUtils.js b/lib/recursorUtils.js index 528197d..5141474 100644 --- a/lib/recursorUtils.js +++ b/lib/recursorUtils.js @@ -24,6 +24,22 @@ function fork (recursor) { } exports.fork = fork +function consumeDeep (recursor) { + const stack = [recursor] + while (stack.length > 0) { + const subject = stack[stack.length - 1]() + if (subject === null) { + stack.pop() + continue + } + + if (typeof subject.createRecursor === 'function') { + stack.push(subject.createRecursor()) + } + } +} +exports.consumeDeep = consumeDeep + function map (recursor, mapFn) { return () => { const next = recursor() diff --git a/lib/serialize.js b/lib/serialize.js index 533c386..4a7e7c5 100644 --- a/lib/serialize.js +++ b/lib/serialize.js @@ -16,6 +16,7 @@ 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') @@ -23,6 +24,7 @@ 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') @@ -33,6 +35,8 @@ 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 @@ -317,12 +321,27 @@ function deserialize (buffer, options) { const descriptorsByPointerIndex = new Map() const mapPointerDescriptor = descriptor => { if (descriptor.isPointer === true) { - if (!descriptorsByPointerIndex.has(descriptor.index)) throw new PointerLookupError(descriptor.index) + 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) + } - return descriptorsByPointerIndex.get(descriptor.index) - } else if (descriptor.isComplex === true) { + if (descriptor.isComplex === true) { descriptorsByPointerIndex.set(descriptor.pointer, descriptor) } + return descriptor } @@ -335,6 +354,8 @@ function deserialize (buffer, options) { return mapPointerDescriptor(deserializeDescriptor(state, recursor)) } } - return deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer) + + const rootDescriptor = deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer) + return rootDescriptor } exports.deserialize = deserialize diff --git a/test/diff.js b/test/diff.js index 8a3e7bb..1b57776 100644 --- a/test/diff.js +++ b/test/diff.js @@ -1,5 +1,6 @@ const test = require('ava') +const concordance = require('..') const { diff: _diff } = require('../lib/diff') const { theme, normalizedTheme, checkThemeUsage } = require('./_instrumentedTheme') @@ -525,3 +526,24 @@ test('objects: effectively resets depth when formatting differences', t => { } t.snapshot(_diff(o1, o2, { maxDepth: 1, theme })) }) + +// See . +test('diff pointers hidden behind maxDepth', t => { + const value = {} + const descriptor = concordance.describe({ + // `value` is encoded in the serialization of `a.b`. `c` is encoded as a + // pointer to the encoded `value`. + a: { + b: value, + }, + c: value, + }) + const serialized = concordance.serialize(descriptor) + + t.notThrows(() => { + // `maxDepth: 1` means that `a.b` is not normally deserialized, and so the + // `c` pointer cannot be resolved, unless the resolution logic first + // deserializes the descriptor in its entirety. + concordance.diffDescriptors(concordance.deserialize(serialized), concordance.describe(undefined), { maxDepth: 1 }) + }) +}) diff --git a/test/format.js b/test/format.js index b92ea6a..f64c59b 100644 --- a/test/format.js +++ b/test/format.js @@ -1,5 +1,6 @@ const test = require('ava') +const concordance = require('..') const { format: _format } = require('../lib/format') const { theme, normalizedTheme, checkThemeUsage } = require('./_instrumentedTheme') const customErrorPlugin = require('./fixtures/customErrorPlugin') @@ -434,3 +435,24 @@ test('format with given plugin', t => { const actual2 = _format(new Error('error'), { plugins, theme }) t.snapshot(actual2) }) + +// See . +test('format pointers hidden behind maxDepth', t => { + const value = {} + const descriptor = concordance.describe({ + // `value` is encoded in the serialization of `a.b`. `c` is encoded as a + // pointer to the encoded `value`. + a: { + b: value, + }, + c: value, + }) + const serialized = concordance.serialize(descriptor) + + t.notThrows(() => { + // `maxDepth: 1` means that `a.b` is not normally deserialized, and so the + // `c` pointer cannot be resolved, unless the resolution logic first + // deserializes the descriptor in its entirety. + concordance.formatDescriptor(concordance.deserialize(serialized), { maxDepth: 1 }) + }) +})