Skip to content

Commit

Permalink
Support for __proto__ as mapping key & anchor identifier (#192)
Browse files Browse the repository at this point in the history
* Allow for __proto__ as an anchor name
* Support __proto__ as key in collectionFromPath()
* Support __proto__ as mapping key
  • Loading branch information
eemeli committed Mar 13, 2021
1 parent abd17b5 commit dc61b13
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 38 deletions.
17 changes: 14 additions & 3 deletions src/ast/Collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ 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 schema.createNode(v, false)
}
Expand Down
9 changes: 7 additions & 2 deletions src/ast/Merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/ast/Pair.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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,
Expand Down Expand Up @@ -58,7 +58,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
}
Expand Down
2 changes: 1 addition & 1 deletion src/doc/Anchors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class Anchors {
)
}

map = {}
map = Object.create(null)

constructor(prefix) {
this.prefix = prefix
Expand Down
2 changes: 1 addition & 1 deletion src/doc/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class Document {
lines.unshift(this.commentBefore.replace(/^/gm, '#'))
}
const ctx = {
anchors: {},
anchors: Object.create(null),
doc: this,
indent: '',
indentStep: ' '.repeat(indentSize),
Expand Down
2 changes: 1 addition & 1 deletion src/doc/createNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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' && prevObjects) {
const prev = prevObjects.get(value)
if (prev) {
Expand Down
46 changes: 26 additions & 20 deletions tests/doc/anchors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -203,35 +225,19 @@ 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' }
}

const expMap = new Map([
[
'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', () => {
Expand Down
7 changes: 7 additions & 0 deletions tests/doc/collection-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,11 @@ describe('Document', () => {
doc.contents = YAML.createNode('s')
expect(() => doc.setIn(['a'], 1)).toThrow(/document contents/)
})

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'
)
})
})
29 changes: 21 additions & 8 deletions tests/doc/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }]
]
Expand Down Expand Up @@ -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]}]')
})
})

0 comments on commit dc61b13

Please sign in to comment.