From 208df066a055ec18753842228790e706a6e3e364 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 21:01:52 +0100 Subject: [PATCH 01/11] refactor to allow objects and references in phpserialised data --- src/core/operations/PHPDeserialize.mjs | 150 +++++++++++++++++++------ 1 file changed, 117 insertions(+), 33 deletions(-) diff --git a/src/core/operations/PHPDeserialize.mjs b/src/core/operations/PHPDeserialize.mjs index 77d18bc2..883a1291 100644 --- a/src/core/operations/PHPDeserialize.mjs +++ b/src/core/operations/PHPDeserialize.mjs @@ -20,7 +20,7 @@ class PHPDeserialize extends Operation { this.name = "PHP Deserialize"; this.module = "Default"; - this.description = "Deserializes PHP serialized data, outputting keyed arrays as JSON.

This function does not support object tags.

Example:
a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}
becomes
{"a": 10,0: {"ab": true}}

Output valid JSON: JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes."; + this.description = "Deserializes PHP serialized data, outputting keyed arrays as JSON.

Example:
a:2:{s:1:"a";i:10;i:0;a:1:{s:2:"ab";b:1;}}
becomes
{"a": 10,0: {"ab": true}}

Output valid JSON: JSON doesn't support integers as keys, whereas PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes."; this.infoURL = "http://www.phpinternalsbook.com/classes_objects/serialization.html"; this.inputType = "string"; this.outputType = "string"; @@ -39,6 +39,8 @@ class PHPDeserialize extends Operation { * @returns {string} */ run(input, args) { + const refStore = []; + const inputPart = input.split(""); /** * Recursive method for deserializing. * @returns {*} @@ -60,7 +62,6 @@ class PHPDeserialize extends Operation { } return result; } - /** * Read characters from the input until `until` is found. * @param until @@ -70,14 +71,10 @@ class PHPDeserialize extends Operation { let result = ""; for (;;) { const char = read(1); - if (char === until) { - break; - } else { - result += char; - } + if (char === until) break; + result += char; } return result; - } /** @@ -85,13 +82,46 @@ class PHPDeserialize extends Operation { * @param expect * @returns {string} */ - function expect(expect) { - const result = read(expect.length); - if (result !== expect) { - throw new OperationError("Unexpected input found"); + function expect(expectStr) { + const result = read(expectStr.length); + if (result !== expectStr) { + throw new OperationError(`Expected "${expectStr}", but got "${result}"`); } return result; } + /** + * Records a value by pushing it into the reference store and returns it. + * @param {any} value - The value to be recorded. + * @returns {any} - The recorded value. + */ + function record(value) { + refStore.push(value); + return value; + } + + /** + * Normalizes the key by converting private and protected keys to standard formats. + * @param {string} key - The key to be normalized. + * @returns {string} - The normalized key. + */ + function normalizeKey(key) { + if (typeof key !== "string") return key; + + // Match private: "\0ClassName\0prop" + const privateMatch = key.match(/^\u0000(.+)\u0000(.+)$/); + if (privateMatch) { + const [_, className, prop] = privateMatch; // eslint-disable-line no-unused-vars + return `private:${prop}`; + } + + // Match protected: "\0*\0prop" + const protectedMatch = key.match(/^\u0000\*\u0000(.+)$/); + if (protectedMatch) { + return `protected:${protectedMatch[1]}`; + } + + return key; + } /** * Helper function to handle deserialized arrays. @@ -100,7 +130,7 @@ class PHPDeserialize extends Operation { function handleArray() { const items = parseInt(readUntil(":"), 10) * 2; expect("{"); - const result = []; + const result = {}; let isKey = true; let lastItem = null; for (let idx = 0; idx < items; idx++) { @@ -109,12 +139,11 @@ class PHPDeserialize extends Operation { lastItem = item; isKey = false; } else { - const numberCheck = lastItem.match(/[0-9]+/); - if (args[0] && numberCheck && numberCheck[0].length === lastItem.length) { - result.push('"' + lastItem + '": ' + item); - } else { - result.push(lastItem + ": " + item); + let key = lastItem; + if (args[0] && typeof key === "number") { + key = key.toString(); } + result[key] = item; isKey = true; } } @@ -122,39 +151,96 @@ class PHPDeserialize extends Operation { return result; } - const kind = read(1).toLowerCase(); switch (kind) { case "n": expect(";"); - return "null"; + return record(null); + case "i": case "d": case "b": { expect(":"); const data = readUntil(";"); if (kind === "b") { - return (parseInt(data, 10) !== 0); + return record(parseInt(data, 10) !== 0); } - return data; + if (kind === "i") { + return record(parseInt(data, 10)); + } + if (kind === "d") { + return record(parseFloat(data)); + } + return record(data); } case "a": expect(":"); - return "{" + handleArray() + "}"; + return record(handleArray()); case "s": { expect(":"); - const length = readUntil(":"); + const lengthRaw = readUntil(":").trim(); + const length = parseInt(lengthRaw, 10); expect("\""); - const value = read(length); - expect('";'); - if (args[0]) { - return '"' + value.replace(/"/g, '\\"') + '"'; // lgtm [js/incomplete-sanitization] - } else { - return '"' + value + '"'; + + // Read until the next quote-semicolon + let str = ""; + while (true) { + const next = read(1); + if (next === '"' && inputPart[0] === ";") { + inputPart.shift(); // Consume the ; + break; + } + str += next; } + + const actualByteLength = new TextEncoder().encode(str).length; + if (actualByteLength !== length) { + // eslint-disable-next-line no-console + console.warn(`Length mismatch: declared ${length}, got ${actualByteLength} — proceeding anyway`); + } + + return record(str); + } + + case "o": { + expect(":"); + const classNameLength = parseInt(readUntil(":"), 10); + expect("\""); + const className = read(classNameLength); + expect("\""); + expect(":"); + const propertyCount = parseInt(readUntil(":"), 10); + expect("{"); + + const obj = { + __className: className + }; + + for (let i = 0; i < propertyCount; i++) { + const keyRaw = handleInput(); + const value = handleInput(); + let key = keyRaw; + if (typeof keyRaw === "string" && keyRaw.startsWith('"') && keyRaw.endsWith('"')) { + key = keyRaw.slice(1, -1); + } + key = normalizeKey(key); + obj[key] = value; + } + + expect("}"); + return record(obj); + } + + case "r": { + expect(":"); + const refIndex = parseInt(readUntil(";"), 10); + if (refIndex >= refStore.length || refIndex < 0) { + throw new OperationError(`Invalid reference index: ${refIndex}`); + } + return refStore[refIndex]; } default: @@ -162,10 +248,8 @@ class PHPDeserialize extends Operation { } } - const inputPart = input.split(""); - return handleInput(); + return JSON.stringify(handleInput()); } - } export default PHPDeserialize; From b13ac0d1105e755b7b98e88ac255413c9787550d Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:18:11 +0100 Subject: [PATCH 02/11] php deserializer fixing tests as its now an object thats being converted to a json string the order of keys is sorted in js --- tests/operations/tests/PHP.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index b9d6a8f0..314af23e 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -46,7 +46,7 @@ TestRegister.addTests([ { name: "PHP Deserialize array (JSON)", input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", - expectedOutput: "{\"a\": 10,\"0\": {\"ab\": true}}", + expectedOutput: "{\"0\": {\"ab\": true},\"a\": 10}", recipeConfig: [ { op: "PHP Deserialize", @@ -57,7 +57,7 @@ TestRegister.addTests([ { name: "PHP Deserialize array (non-JSON)", input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", - expectedOutput: "{\"a\": 10,0: {\"ab\": true}}", + expectedOutput: "{0: {\"ab\": true},\"a\": 10}", recipeConfig: [ { op: "PHP Deserialize", From d1a22e34b9c0b7f23e0ef18893e955d5b626b56d Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:27:44 +0100 Subject: [PATCH 03/11] adding more tests + fixing again --- tests/operations/tests/PHP.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index 314af23e..30ec65d4 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -46,7 +46,7 @@ TestRegister.addTests([ { name: "PHP Deserialize array (JSON)", input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", - expectedOutput: "{\"0\": {\"ab\": true},\"a\": 10}", + expectedOutput: '{"0":{"ab":true},"a":10}', recipeConfig: [ { op: "PHP Deserialize", @@ -57,7 +57,7 @@ TestRegister.addTests([ { name: "PHP Deserialize array (non-JSON)", input: "a:2:{s:1:\"a\";i:10;i:0;a:1:{s:2:\"ab\";b:1;}}", - expectedOutput: "{0: {\"ab\": true},\"a\": 10}", + expectedOutput: '{0:{"ab":true},"a":10}', recipeConfig: [ { op: "PHP Deserialize", @@ -65,4 +65,15 @@ TestRegister.addTests([ }, ], }, + { + name: "PHP Deserialize array with object and reference", + input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:7:"�*�name";s:16:"Secondary Navbar";s:11:"�*�children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', + expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', + recipeConfig: [ + { + op: "PHP Deserialize", + args: [false], + }, + ], + } ]); From e91f17a4e095d89d9f5b7a649189a02b7edb3114 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:31:20 +0100 Subject: [PATCH 04/11] Update PHP.mjs --- tests/operations/tests/PHP.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index 30ec65d4..b753d926 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -68,7 +68,7 @@ TestRegister.addTests([ { name: "PHP Deserialize array with object and reference", input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:7:"�*�name";s:16:"Secondary Navbar";s:11:"�*�children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', - expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', + expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', // eslint-disable-next-line no-useless-escape recipeConfig: [ { op: "PHP Deserialize", From 5a78c2af899bca94dfc6863ec8e7e9b864d74975 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:32:32 +0100 Subject: [PATCH 05/11] Update PHP.mjs --- tests/operations/tests/PHP.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index b753d926..809081c4 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -68,7 +68,8 @@ TestRegister.addTests([ { name: "PHP Deserialize array with object and reference", input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:7:"�*�name";s:16:"Secondary Navbar";s:11:"�*�children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', - expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', // eslint-disable-next-line no-useless-escape + // eslint-disable-next-line no-useless-escape + expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', recipeConfig: [ { op: "PHP Deserialize", From 48e7eae9bc7fe28d3d54f47ad95c34e00b1aa030 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:35:48 +0100 Subject: [PATCH 06/11] Update PHP.mjs --- tests/operations/tests/PHP.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index 809081c4..b7c27b41 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -67,9 +67,9 @@ TestRegister.addTests([ }, { name: "PHP Deserialize array with object and reference", - input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:7:"�*�name";s:16:"Secondary Navbar";s:11:"�*�children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', + input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', // eslint-disable-next-line no-useless-escape - expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","�*�name":"Secondary Navbar","�*�children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","�*�name":"View Cart","�*�parent":"Secondary Navbar"}},"�*�parent":null}}', + expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}', recipeConfig: [ { op: "PHP Deserialize", From 0b69a04eb71576ce969cb9b7a8b1bf376b945883 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:41:41 +0100 Subject: [PATCH 07/11] Update PHP.mjs --- tests/operations/tests/PHP.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index b7c27b41..703bdd7e 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -67,9 +67,8 @@ TestRegister.addTests([ }, { name: "PHP Deserialize array with object and reference", - input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:7:"�*�name";s:9:"View Cart";s:9:"�*�parent";r:2;}}s:9:"�*�parent";N;}}', - // eslint-disable-next-line no-useless-escape - expectedOutput: '{"navbar":{"__className":"APP\\View\\Menu\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}', + input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:4:"name";s:9:"View Cart";s:6:"parent";r:2;}}s:6:"parent";N;}}', // eslint-disable-line no-useless-escape + expectedOutput: `{"navbar":{"__className":"APP\\View\\Menu\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}`, recipeConfig: [ { op: "PHP Deserialize", From 7b28a1027ca6347c9a0d3b967c92d47ce961ceb9 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 22:44:23 +0100 Subject: [PATCH 08/11] Update PHP.mjs --- tests/operations/tests/PHP.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index 703bdd7e..0180981b 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -67,8 +67,8 @@ TestRegister.addTests([ }, { name: "PHP Deserialize array with object and reference", - input: 'a:1:{s:6:"navbar";O:18:"APP\View\Menu\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\View\Menu\Item":2:{s:4:"name";s:9:"View Cart";s:6:"parent";r:2;}}s:6:"parent";N;}}', // eslint-disable-line no-useless-escape - expectedOutput: `{"navbar":{"__className":"APP\\View\\Menu\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\View\\Menu\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}`, + input: 'a:1:{s:6:"navbar";O:18:"APP\\View\\Menu\\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\\View\\Menu\\Item":2:{s:4:"name";s:9:"View Cart";s:6:"parent";r:2;}}s:6:"parent";N;}}', // eslint-disable-line no-useless-escape + expectedOutput: `{"navbar":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}`, recipeConfig: [ { op: "PHP Deserialize", From d713bbad3a9ef28681d8cdbec88dff728b7e277a Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 23:46:20 +0100 Subject: [PATCH 09/11] refactor to have support for past output style --- src/core/operations/PHPDeserialize.mjs | 92 +++++++++++++++----------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/src/core/operations/PHPDeserialize.mjs b/src/core/operations/PHPDeserialize.mjs index 883a1291..41f4e66a 100644 --- a/src/core/operations/PHPDeserialize.mjs +++ b/src/core/operations/PHPDeserialize.mjs @@ -79,7 +79,7 @@ class PHPDeserialize extends Operation { /** * Read characters from the input that must be equal to `expect` - * @param expect + * @param expectStr * @returns {string} */ function expect(expectStr) { @@ -131,21 +131,12 @@ class PHPDeserialize extends Operation { const items = parseInt(readUntil(":"), 10) * 2; expect("{"); const result = {}; - let isKey = true; - let lastItem = null; - for (let idx = 0; idx < items; idx++) { - const item = handleInput(); - if (isKey) { - lastItem = item; - isKey = false; - } else { - let key = lastItem; - if (args[0] && typeof key === "number") { - key = key.toString(); - } - result[key] = item; - isKey = true; - } + for (let idx = 0; idx < items; idx += 2) { + const keyInfo = handleInput(); + const valueInfo = handleInput(); + let key = keyInfo.value; + if (keyInfo.keyType === "i") key = parseInt(key, 10); + result[key] = valueInfo.value; } expect("}"); return result; @@ -156,28 +147,29 @@ class PHPDeserialize extends Operation { switch (kind) { case "n": expect(";"); - return record(null); + return record({ value: null, keyType: kind }); + + case "i": { + expect(":"); + const data = readUntil(";"); + return record({ value: parseInt(data, 10), keyType: kind }); + } + + case "d": { + expect(":"); + const data = readUntil(";"); + return record({ value: parseFloat(data), keyType: kind }); + } - case "i": - case "d": case "b": { expect(":"); const data = readUntil(";"); - if (kind === "b") { - return record(parseInt(data, 10) !== 0); - } - if (kind === "i") { - return record(parseInt(data, 10)); - } - if (kind === "d") { - return record(parseFloat(data)); - } - return record(data); + return record({ value: data !== 0, keyType: kind }); } case "a": expect(":"); - return record(handleArray()); + return record({ value: handleArray(), keyType: kind }); case "s": { expect(":"); @@ -202,7 +194,7 @@ class PHPDeserialize extends Operation { console.warn(`Length mismatch: declared ${length}, got ${actualByteLength} — proceeding anyway`); } - return record(str); + return record({ value: str, keyType: kind }); } case "o": { @@ -221,17 +213,17 @@ class PHPDeserialize extends Operation { for (let i = 0; i < propertyCount; i++) { const keyRaw = handleInput(); - const value = handleInput(); - let key = keyRaw; - if (typeof keyRaw === "string" && keyRaw.startsWith('"') && keyRaw.endsWith('"')) { - key = keyRaw.slice(1, -1); + const valueRaw = handleInput(); + let key = keyRaw.value; + if (typeof key === "string" && key.startsWith('"') && key.endsWith('"')) { + key = key.slice(1, -1); } key = normalizeKey(key); - obj[key] = value; + obj[key] = valueRaw.value; } expect("}"); - return record(obj); + return record({ value: obj, keyType: kind }); } case "r": { @@ -240,7 +232,11 @@ class PHPDeserialize extends Operation { if (refIndex >= refStore.length || refIndex < 0) { throw new OperationError(`Invalid reference index: ${refIndex}`); } - return refStore[refIndex]; + const refValue = refStore[refIndex]; + if (typeof refValue === "object" && refValue !== null && "value" in refValue && "keyType" in refValue) { + return refValue; + } + return record({ value: refValue, keyType: kind }); } default: @@ -248,7 +244,25 @@ class PHPDeserialize extends Operation { } } - return JSON.stringify(handleInput()); + /** + * Helper function to make invalid json output (legacy support) + * @returns {String} + */ + function stringifyWithIntegerKeys(obj) { + const entries = Object.entries(obj).map(([key, value]) => { + const jsonKey = Number.isInteger(+key) ? key : JSON.stringify(key); + const jsonValue = JSON.stringify(value); + return `${jsonKey}:${jsonValue}`; + }); + return `{${entries.join(',')}}`; // eslint-disable-line quotes + } + + if (args[0]) { + return JSON.stringify(handleInput().value); + + } else { + return stringifyWithIntegerKeys(handleInput().value); + } } } From ea5a17c26d6ac80a18dc549407a09c24baae1413 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 23:48:05 +0100 Subject: [PATCH 10/11] adjust test --- tests/operations/tests/PHP.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/operations/tests/PHP.mjs b/tests/operations/tests/PHP.mjs index 0180981b..65085946 100644 --- a/tests/operations/tests/PHP.mjs +++ b/tests/operations/tests/PHP.mjs @@ -66,13 +66,13 @@ TestRegister.addTests([ ], }, { - name: "PHP Deserialize array with object and reference", + name: "PHP Deserialize array with object and reference (JSON)", input: 'a:1:{s:6:"navbar";O:18:"APP\\View\\Menu\\Item":3:{s:4:"name";s:16:"Secondary Navbar";s:8:"children";a:1:{s:9:"View Cart";O:18:"APP\\View\\Menu\\Item":2:{s:4:"name";s:9:"View Cart";s:6:"parent";r:2;}}s:6:"parent";N;}}', // eslint-disable-line no-useless-escape expectedOutput: `{"navbar":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"Secondary Navbar","children":{"View Cart":{"__className":"APP\\\\View\\\\Menu\\\\Item","name":"View Cart","parent":"Secondary Navbar"}},"parent":null}}`, recipeConfig: [ { op: "PHP Deserialize", - args: [false], + args: [true], }, ], } From e77cdaee1138dfb0a6f2297deb4216e83ef7e9fc Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" Date: Tue, 13 May 2025 23:51:57 +0100 Subject: [PATCH 11/11] linting fix --- src/core/operations/PHPDeserialize.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/operations/PHPDeserialize.mjs b/src/core/operations/PHPDeserialize.mjs index 41f4e66a..bd820375 100644 --- a/src/core/operations/PHPDeserialize.mjs +++ b/src/core/operations/PHPDeserialize.mjs @@ -259,7 +259,6 @@ class PHPDeserialize extends Operation { if (args[0]) { return JSON.stringify(handleInput().value); - } else { return stringifyWithIntegerKeys(handleInput().value); }