diff --git a/src/lib/plist/plist-parser.ts b/src/lib/plist/plist-parser.ts index 85d2f880..4774a21e 100644 --- a/src/lib/plist/plist-parser.ts +++ b/src/lib/plist/plist-parser.ts @@ -111,14 +111,23 @@ export function parsePlist(xmlData: string | Buffer): PlistDictionary { /** * Parse a plist `` element into a JavaScript object. + * Only direct-child `` elements are considered: getElementsByTagName + * is recursive and would flatten keys of nested dicts into the parent. */ function parseDict(dictNode: Element): PlistDictionary { const obj: PlistDictionary = {}; - const keys = dictNode.getElementsByTagName('key'); + let childNode = dictNode.firstChild; - for (let i = 0; i < keys.length; i++) { - const keyName = keys[i].textContent || ''; - let valueNode = keys[i].nextSibling; + while (childNode) { + const keyNode = childNode; + childNode = childNode.nextSibling; + + if (keyNode.nodeType !== Node.ELEMENT_NODE || keyNode.nodeName !== 'key') { + continue; + } + + const keyName = keyNode.textContent || ''; + let valueNode = keyNode.nextSibling; while (valueNode && valueNode.nodeType !== Node.ELEMENT_NODE) { valueNode = valueNode.nextSibling; @@ -126,6 +135,8 @@ export function parsePlist(xmlData: string | Buffer): PlistDictionary { if (valueNode) { obj[keyName] = parseNode(valueNode as Element); + // Skip ahead of the parsed value so the loop doesn't re-visit it + childNode = valueNode.nextSibling; } } diff --git a/test/unit/plist/plist-parser.spec.ts b/test/unit/plist/plist-parser.spec.ts index bffe74dc..9f1c57e7 100644 --- a/test/unit/plist/plist-parser.spec.ts +++ b/test/unit/plist/plist-parser.spec.ts @@ -133,6 +133,71 @@ describe('Plist Parser', function () { const level2 = level1.level2 as PlistDictionary; assert.strictEqual(level2.level3, 'deep value'); + + // Nested keys must not leak into ancestor dicts + assert.deepStrictEqual(Object.keys(result), ['level1']); + assert.deepStrictEqual(Object.keys(level1), ['level2']); + assert.deepStrictEqual(Object.keys(level2), ['level3']); + }); + + it('should not let a nested key overwrite an outer key with the same name', function () { + const xml = ` + + + + Result + ok + Nested + + Result + 1 + Inner + x + + Name + dev + + + `; + + const result = parseXmlPlist(xml); + assert.strictEqual(result.Result, 'ok'); + assert.strictEqual(result.Name, 'dev'); + assert.ok(!('Inner' in result)); + + const nested = result.Nested as PlistDictionary; + assert.strictEqual(nested.Result, 1); + assert.strictEqual(nested.Inner, 'x'); + }); + + it('should not leak keys from dicts nested inside arrays', function () { + const xml = ` + + + + Status + Complete + Items + + + Status + Pending + Id + 7 + + + + + `; + + const result = parseXmlPlist(xml); + assert.strictEqual(result.Status, 'Complete'); + assert.deepStrictEqual(Object.keys(result), ['Status', 'Items']); + + const items = result.Items as PlistArray; + const item = items[0] as PlistDictionary; + assert.strictEqual(item.Status, 'Pending'); + assert.strictEqual(item.Id, 7); }); it('should parse mixed arrays and dictionaries', function () {