diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index f04b5fd9..09f0187c 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -66,6 +66,7 @@ const Categories = [ "Encode text", "Decode text", "Swap endianness", + "PHP Deserialize", ] }, { diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 9caa4f91..469a98c1 100755 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -35,6 +35,7 @@ import StrUtils from "../operations/StrUtils.js"; import Tidy from "../operations/Tidy.js"; import Unicode from "../operations/Unicode.js"; import URL_ from "../operations/URL.js"; +import PhpSerialization from "../operations/PhpSerialization.js"; /** @@ -3845,6 +3846,19 @@ const OperationConfig = { } ] }, + "PHP Deserialize": { + module: "Default", + description: "PHP Deserialize a given input.

This function does not support object tags.

Output valid JSON: JSON doesn't support integers as keys, where as PHP serialization does. Enabling this will cast these integers to strings. This will also escape backslashes.", + inputType: "string", + outputType: "string", + args: [ + { + name: "Output valid JSON", + type: "boolean", + value: PhpSerialization.OUTPUT_VALID_JSON + } + ] + }, }; diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js index 682db223..8c13cfd2 100644 --- a/src/core/config/modules/Default.js +++ b/src/core/config/modules/Default.js @@ -27,7 +27,7 @@ import StrUtils from "../../operations/StrUtils.js"; import Tidy from "../../operations/Tidy.js"; import Unicode from "../../operations/Unicode.js"; import UUID from "../../operations/UUID.js"; - +import PhpSerialization from "../../operations/PhpSerialization"; /** * Default module. @@ -155,6 +155,7 @@ OpModules.Default = { "Conditional Jump": FlowControl.runCondJump, "Return": FlowControl.runReturn, "Comment": FlowControl.runComment, + "PHP Deserialize": PhpSerialization.PhpDeserialize, /* diff --git a/src/core/operations/PhpSerialization.js b/src/core/operations/PhpSerialization.js new file mode 100644 index 00000000..3b2d8a20 --- /dev/null +++ b/src/core/operations/PhpSerialization.js @@ -0,0 +1,158 @@ +/** + * Php Serialization operations. + * This Javascript implementation is based on the Python implementation by + * Armin Ronacher (2016), who released it under the 3-Clause BSD license. + * See: https://github.com/mitsuhiko/phpserialize/ + * + * @author Jarmo van Lenthe [github.com/jarmovanlenthe] + * @copyright Jarmo van Lenthe + * @license Apache-2.0 + * + * @namespace + */ + +const PhpSerialization = { + + /** + * @constant + * @default + */ + OUTPUT_VALID_JSON: true, + + /** + * Deserializes a PHP serialized input + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + PhpDeserialize: function (input, args) { + /** + * Recursive method for deserializing. + * @returns {*} + */ + function handleInput() { + /** + * Read `length` characters from the input, shifting them out the input. + * @param length + * @returns {string} + */ + function read(length) { + let result = ""; + for (let idx = 0; idx < length; idx++) { + let char = inputPart.shift(); + if (char === undefined) { + throw "End of input reached before end of script"; + } + result += char; + } + return result; + } + + /** + * Read characters from the input until `until` is found. + * @param until + * @returns {string} + */ + function readUntil(until) { + let result = ""; + for (;;) { + let char = read(1); + if (char === until) { + break; + } else { + result += char; + } + } + return result; + + } + + /** + * Read characters from the input that must be equal to `expect` + * @param expect + * @returns {string} + */ + function expect(expect) { + let result = read(expect.length); + if (result !== expect) { + throw "Unexpected input found"; + } + return result; + } + + /** + * Helper function to handle deserialized arrays. + * @returns {Array} + */ + function handleArray() { + let items = parseInt(readUntil(":"), 10) * 2; + expect("{"); + let result = []; + let isKey = true; + let lastItem = null; + for (let idx = 0; idx < items; idx++) { + let item = handleInput(); + if (isKey) { + lastItem = item; + isKey = false; + } else { + let numberCheck = lastItem.match(/[0-9]+/); + if (args[0] && numberCheck && numberCheck[0].length === lastItem.length) { + result.push("\"" + lastItem + "\": " + item); + } else { + result.push(lastItem + ": " + item); + } + isKey = true; + } + } + expect("}"); + return result; + } + + + let kind = read(1).toLowerCase(); + + switch (kind) { + case "n": + expect(";"); + return ""; + + case "i": + case "d": + case "b": { + expect(":"); + let data = readUntil(";"); + if (kind === "b") { + return (parseInt(data, 10) !== 0); + } + return data; + } + + case "a": + expect(":"); + return "{" + handleArray() + "}"; + + case "s": { + expect(":"); + let length = readUntil(":"); + expect("\""); + let value = read(length); + expect("\";"); + if (args[0]) { + return "\"" + value.replace(/"/g, "\\\"") + "\""; + } else { + return "\"" + value + "\""; + } + } + + default: + throw "Unknown type: " + kind; + } + } + + let inputPart = input.split(""); + return handleInput(); + } +}; + +export default PhpSerialization; diff --git a/test/index.js b/test/index.js index 773a5b14..9fa4dcdc 100644 --- a/test/index.js +++ b/test/index.js @@ -27,6 +27,7 @@ import "./tests/operations/MorseCode.js"; import "./tests/operations/MS.js"; import "./tests/operations/StrUtils.js"; import "./tests/operations/SeqUtils.js"; +import "./tests/operations/PhpSerialization.js"; let allTestsPassing = true; diff --git a/test/tests/operations/PhpSerialization.js b/test/tests/operations/PhpSerialization.js new file mode 100644 index 00000000..8d745df9 --- /dev/null +++ b/test/tests/operations/PhpSerialization.js @@ -0,0 +1,68 @@ +/** + * PHP Serialization tests. + * + * @author Jarmo van Lenthe + * + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import TestRegister from "../../TestRegister.js"; + +TestRegister.addTests([ + { + name: "PHP Deserialize empty array", + input: "a:0:{}", + expectedOutput: "{}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize integer", + input: "i:10;", + expectedOutput: "10", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + name: "PHP Deserialize string", + input: "s:17:\"PHP Serialization\";", + expectedOutput: "\"PHP Serialization\"", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + 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}}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [true], + }, + ], + }, + { + 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}}", + recipeConfig: [ + { + op: "PHP Deserialize", + args: [false], + }, + ], + }, +]);