From 4ab632e3f4c7dfdf267bcd2ba7f14f9f62b0655c Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Sun, 6 Sep 2020 20:27:06 +0300 Subject: [PATCH] Support for __proto__ as mapping key & anchor identifier (#192) * Allow for __proto__ as an anchor name * Support __proto__ as key in collectionFromPath() * Support __proto__ as mapping key --- src/ast/Collection.js | 17 ++++++++++--- src/ast/Merge.js | 9 +++++-- src/ast/Pair.js | 12 +++++++-- src/doc/Anchors.js | 2 +- src/doc/Document.js | 2 +- src/doc/createNode.js | 2 +- tests/doc/anchors.js | 46 +++++++++++++++++++--------------- tests/doc/collection-access.js | 7 ++++++ tests/doc/parse.js | 29 +++++++++++++++------ 9 files changed, 88 insertions(+), 38 deletions(-) diff --git a/src/ast/Collection.js b/src/ast/Collection.js index 9e40973d..06204604 100644 --- a/src/ast/Collection.js +++ b/src/ast/Collection.js @@ -8,9 +8,20 @@ export function collectionFromPath(schema, path, value) { let v = value for (let i = path.length - 1; i >= 0; --i) { const k = path[i] - const o = Number.isInteger(k) && k >= 0 ? [] : {} - o[k] = v - v = o + if (Number.isInteger(k) && k >= 0) { + const a = [] + a[k] = v + v = a + } else { + const o = {} + Object.defineProperty(o, k, { + value: v, + writable: true, + enumerable: true, + configurable: true + }) + v = o + } } return createNode(v, null, { onAlias() { diff --git a/src/ast/Merge.js b/src/ast/Merge.js index 45d07f9f..9c93e826 100644 --- a/src/ast/Merge.js +++ b/src/ast/Merge.js @@ -39,8 +39,13 @@ export class Merge extends Pair { if (!map.has(key)) map.set(key, value) } else if (map instanceof Set) { map.add(key) - } else { - if (!Object.prototype.hasOwnProperty.call(map, key)) map[key] = value + } else if (!Object.prototype.hasOwnProperty.call(map, key)) { + Object.defineProperty(map, key, { + value, + writable: true, + enumerable: true, + configurable: true + }) } } } diff --git a/src/ast/Pair.js b/src/ast/Pair.js index 70ef411c..5a00be29 100644 --- a/src/ast/Pair.js +++ b/src/ast/Pair.js @@ -12,7 +12,7 @@ const stringifyKey = (key, jsKey, ctx) => { if (typeof jsKey !== 'object') return String(jsKey) if (key instanceof Node && ctx && ctx.doc) return key.toString({ - anchors: {}, + anchors: Object.create(null), doc: ctx.doc, indent: '', indentStep: ctx.indentStep, @@ -65,7 +65,15 @@ export class Pair extends Node { map.add(key) } else { const stringKey = stringifyKey(this.key, key, ctx) - map[stringKey] = toJSON(this.value, stringKey, ctx) + const value = toJSON(this.value, stringKey, ctx) + if (stringKey in map) + Object.defineProperty(map, stringKey, { + value, + writable: true, + enumerable: true, + configurable: true + }) + else map[stringKey] = value } return map } diff --git a/src/doc/Anchors.js b/src/doc/Anchors.js index a8b366df..56f9b6a6 100644 --- a/src/doc/Anchors.js +++ b/src/doc/Anchors.js @@ -9,7 +9,7 @@ export class Anchors { ) } - map = {} + map = Object.create(null) constructor(prefix) { this.prefix = prefix diff --git a/src/doc/Document.js b/src/doc/Document.js index 62e8f023..7e9cdb7f 100644 --- a/src/doc/Document.js +++ b/src/doc/Document.js @@ -291,7 +291,7 @@ export class Document { lines.unshift(this.commentBefore.replace(/^/gm, '#')) } const ctx = { - anchors: {}, + anchors: Object.create(null), doc: this, indent: '', indentStep: ' '.repeat(indentSize), diff --git a/src/doc/createNode.js b/src/doc/createNode.js index 48d9b7aa..e5d9ce14 100644 --- a/src/doc/createNode.js +++ b/src/doc/createNode.js @@ -33,7 +33,7 @@ export function createNode(value, tagName, ctx) { // Detect duplicate references to the same object & use Alias nodes for all // after first. The `obj` wrapper allows for circular references to resolve. - const obj = {} + const obj = { value: undefined, node: undefined } if (value && typeof value === 'object') { const prev = prevObjects.get(value) if (prev) return onAlias(prev) diff --git a/tests/doc/anchors.js b/tests/doc/anchors.js index 1b6083b4..3ba789ca 100644 --- a/tests/doc/anchors.js +++ b/tests/doc/anchors.js @@ -82,6 +82,28 @@ describe('create', () => { }) }) +describe('__proto__ as anchor name', () => { + test('parse', () => { + const src = `- &__proto__ 1\n- *__proto__\n` + const doc = YAML.parseDocument(src) + expect(doc.errors).toHaveLength(0) + const { items } = doc.contents + expect(items).toMatchObject([{ value: 1 }, { source: { value: 1 } }]) + expect(items[1].source).toBe(items[0]) + expect(String(doc)).toBe(src) + }) + + test('create/stringify', () => { + const doc = YAML.parseDocument('[{ a: A }, { b: B }]') + const alias = doc.anchors.createAlias(doc.contents.items[0], '__proto__') + doc.contents.items.push(alias) + expect(doc.toJSON()).toMatchObject([{ a: 'A' }, { b: 'B' }, { a: 'A' }]) + expect(String(doc)).toMatch( + '[ &__proto__ { a: A }, { b: B }, *__proto__ ]\n' + ) + }) +}) + describe('merge <<', () => { const src = `--- - &CENTER { x: 1, y: 2 } @@ -203,10 +225,7 @@ y: <<: [ *a, *b ]` const expObj = { - x: [ - { k0: 'v1', k1: 'v1' }, - { k1: 'v2', k2: 'v2' } - ], + x: [{ k0: 'v1', k1: 'v1' }, { k1: 'v2', k2: 'v2' }], y: { k0: 'v0', k1: 'v1', k2: 'v2' } } @@ -214,24 +233,11 @@ y: [ 'x', [ - new Map([ - ['k0', 'v1'], - ['k1', 'v1'] - ]), - new Map([ - ['k1', 'v2'], - ['k2', 'v2'] - ]) + new Map([['k0', 'v1'], ['k1', 'v1']]), + new Map([['k1', 'v2'], ['k2', 'v2']]) ] ], - [ - 'y', - new Map([ - ['k0', 'v0'], - ['k1', 'v1'], - ['k2', 'v2'] - ]) - ] + ['y', new Map([['k0', 'v0'], ['k1', 'v1'], ['k2', 'v2']])] ]) test('multiple merge keys, masAsMap: false', () => { diff --git a/tests/doc/collection-access.js b/tests/doc/collection-access.js index 86a83e79..662f34de 100644 --- a/tests/doc/collection-access.js +++ b/tests/doc/collection-access.js @@ -514,4 +514,11 @@ describe('Document', () => { doc.setIn(['c', 1], 9) expect(String(doc)).toBe('{ a: 1, b: [ 2, 3 ], c: [ null, 9 ] }\n') }) + + test('setIn with __proto__ as key', () => { + doc.setIn(['c', '__proto__'], 9) + expect(String(doc)).toBe( + 'a: 1\nb:\n - 2\n - 3\nc:\n __proto__: 9\n' + ) + }) }) diff --git a/tests/doc/parse.js b/tests/doc/parse.js index e8e0452c..21cfb6db 100644 --- a/tests/doc/parse.js +++ b/tests/doc/parse.js @@ -396,10 +396,7 @@ test('eemeli/yaml#38', () => { expect(YAML.parse(src)).toEqual({ content: { arrayOfArray: [ - [ - { first: 'John', last: 'Black' }, - { first: 'Brian', last: 'Green' } - ], + [{ first: 'John', last: 'Black' }, { first: 'Brian', last: 'Green' }], [{ first: 'Mark', last: 'Orange' }], [{ first: 'Adam', last: 'Grey' }] ] @@ -607,8 +604,24 @@ test('Document.toJSON(null, onAnchor)', () => { const doc = YAML.parseDocument(src) const onAnchor = jest.fn() const res = doc.toJSON(null, onAnchor) - expect(onAnchor.mock.calls).toMatchObject([ - [res.foo, 3], - ['foo', 1] - ]) + expect(onAnchor.mock.calls).toMatchObject([[res.foo, 3], ['foo', 1]]) +}) + +describe('__proto__ as mapping key', () => { + test('plain object', () => { + const src = '{ __proto__: [42] }' + const obj = YAML.parse(src) + expect(Array.isArray(obj)).toBe(false) + expect(obj.hasOwnProperty('__proto__')).toBe(true) + expect(obj).not.toHaveProperty('length') + expect(JSON.stringify(obj)).toBe('{"__proto__":[42]}') + }) + + test('with merge key', () => { + const src = '- &A { __proto__: [42] }\n- { <<: *A }\n' + const obj = YAML.parse(src, { merge: true }) + expect(obj[0].hasOwnProperty('__proto__')).toBe(true) + expect(obj[1].hasOwnProperty('__proto__')).toBe(true) + expect(JSON.stringify(obj)).toBe('[{"__proto__":[42]},{"__proto__":[42]}]') + }) })