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;