瀏覽代碼

Merge branch 'artemisbot-esmconversion' into esm

n1474335 7 年之前
父節點
當前提交
40b29d770a

File diff suppressed because it is too large
+ 241 - 241
package-lock.json


+ 72 - 0
src/core/lib/PublicKey.mjs

@@ -0,0 +1,72 @@
+/**
+ * Public key resources.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import { toHex, fromHex } from "./Hex";
+
+/**
+ * Formats Distinguished Name (DN) strings.
+ *
+ * @param {string} dnStr
+ * @param {number} indent
+ * @returns {string}
+ */
+export function formatDnStr (dnStr, indent) {
+    const fields = dnStr.substr(1).replace(/([^\\])\//g, "$1$1/").split(/[^\\]\//);
+    let output = "",
+        maxKeyLen = 0,
+        key,
+        value,
+        i,
+        str;
+
+    for (i = 0; i < fields.length; i++) {
+        if (!fields[i].length) continue;
+
+        key = fields[i].split("=")[0];
+
+        maxKeyLen = key.length > maxKeyLen ? key.length : maxKeyLen;
+    }
+
+    for (i = 0; i < fields.length; i++) {
+        if (!fields[i].length) continue;
+
+        key = fields[i].split("=")[0];
+        value = fields[i].split("=")[1];
+        str = key.padEnd(maxKeyLen, " ") + " = " + value + "\n";
+
+        output += str.padStart(indent + str.length, " ");
+    }
+
+    return output.slice(0, -1);
+}
+
+
+/**
+ * Formats byte strings by adding line breaks and delimiters.
+ *
+ * @param {string} byteStr
+ * @param {number} length - Line width
+ * @param {number} indent
+ * @returns {string}
+ */
+export function formatByteStr (byteStr, length, indent) {
+    byteStr = toHex(fromHex(byteStr), ":");
+    length = length * 3;
+    let output = "";
+
+    for (let i = 0; i < byteStr.length; i += length) {
+        const str = byteStr.slice(i, i + length) + "\n";
+        if (i === 0) {
+            output += str;
+        } else {
+            output += str.padStart(indent + str.length, " ");
+        }
+    }
+
+    return output.slice(0, output.length-1);
+}

+ 47 - 0
src/core/operations/CSSBeautify.mjs

@@ -0,0 +1,47 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * CSS Beautify operation
+ */
+class CSSBeautify extends Operation {
+
+    /**
+     * CSSBeautify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "CSS Beautify";
+        this.module = "Code";
+        this.description = "Indents and prettifies Cascading Style Sheets (CSS) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Indent string",
+                "type": "binaryShortString",
+                "value": "\\t"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const indentStr = args[0];
+        return vkbeautify.css(input, indentStr);
+    }
+
+}
+
+export default CSSBeautify;

+ 47 - 0
src/core/operations/CSSMinify.mjs

@@ -0,0 +1,47 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * CSS Minify operation
+ */
+class CSSMinify extends Operation {
+
+    /**
+     * CSSMinify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "CSS Minify";
+        this.module = "Code";
+        this.description = "Compresses Cascading Style Sheets (CSS) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Preserve comments",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const preserveComments = args[0];
+        return vkbeautify.cssmin(input, preserveComments);
+    }
+
+}
+
+export default CSSMinify;

+ 90 - 0
src/core/operations/CSSSelector.mjs

@@ -0,0 +1,90 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import xmldom from "xmldom";
+import nwmatcher from "nwmatcher";
+
+/**
+ * CSS selector operation
+ */
+class CSSSelector extends Operation {
+
+    /**
+     * CSSSelector constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "CSS selector";
+        this.module = "Code";
+        this.description = "Extract information from an HTML document with a CSS selector";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "CSS selector",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Delimiter",
+                "type": "binaryShortString",
+                "value": "\\n"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [query, delimiter] = args,
+            parser = new xmldom.DOMParser();
+        let dom,
+            result;
+
+        if (!query.length || !input.length) {
+            return "";
+        }
+
+        try {
+            dom = parser.parseFromString(input);
+        } catch (err) {
+            throw new OperationError("Invalid input HTML.");
+        }
+
+        try {
+            const matcher = nwmatcher({document: dom});
+            result = matcher.select(query, dom);
+        } catch (err) {
+            throw new OperationError("Invalid CSS Selector. Details:\n" + err.message);
+        }
+
+        const nodeToString = function(node) {
+            return node.toString();
+            /* xmldom does not return the outerHTML value.
+            switch (node.nodeType) {
+                case node.ELEMENT_NODE: return node.outerHTML;
+                case node.ATTRIBUTE_NODE: return node.value;
+                case node.TEXT_NODE: return node.wholeText;
+                case node.COMMENT_NODE: return node.data;
+                case node.DOCUMENT_NODE: return node.outerHTML;
+                default: throw new Error("Unknown Node Type: " + node.nodeType);
+            }*/
+        };
+
+        return result
+            .map(nodeToString)
+            .join(delimiter);
+    }
+
+}
+
+export default CSSSelector;

+ 11 - 8
src/core/operations/Diff.mjs

@@ -71,36 +71,39 @@ class Diff extends Operation {
         let output = "",
             diff;
 
+        // Node and Webpack load modules slightly differently
+        const jsdiff = JsDiff.default ? JsDiff.default : JsDiff;
+
         if (!samples || samples.length !== 2) {
             throw new OperationError("Incorrect number of samples, perhaps you need to modify the sample delimiter or add more samples?");
         }
 
         switch (diffBy) {
             case "Character":
-                diff = JsDiff.diffChars(samples[0], samples[1]);
+                diff = jsdiff.diffChars(samples[0], samples[1]);
                 break;
             case "Word":
                 if (ignoreWhitespace) {
-                    diff = JsDiff.diffWords(samples[0], samples[1]);
+                    diff = jsdiff.diffWords(samples[0], samples[1]);
                 } else {
-                    diff = JsDiff.diffWordsWithSpace(samples[0], samples[1]);
+                    diff = jsdiff.diffWordsWithSpace(samples[0], samples[1]);
                 }
                 break;
             case "Line":
                 if (ignoreWhitespace) {
-                    diff = JsDiff.diffTrimmedLines(samples[0], samples[1]);
+                    diff = jsdiff.diffTrimmedLines(samples[0], samples[1]);
                 } else {
-                    diff = JsDiff.diffLines(samples[0], samples[1]);
+                    diff = jsdiff.diffLines(samples[0], samples[1]);
                 }
                 break;
             case "Sentence":
-                diff = JsDiff.diffSentences(samples[0], samples[1]);
+                diff = jsdiff.diffSentences(samples[0], samples[1]);
                 break;
             case "CSS":
-                diff = JsDiff.diffCss(samples[0], samples[1]);
+                diff = jsdiff.diffCss(samples[0], samples[1]);
                 break;
             case "JSON":
-                diff = JsDiff.diffJson(samples[0], samples[1]);
+                diff = jsdiff.diffJson(samples[0], samples[1]);
                 break;
             default:
                 throw new OperationError("Invalid 'Diff by' option.");

+ 62 - 0
src/core/operations/ExtractEXIF.mjs

@@ -0,0 +1,62 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import ExifParser from "exif-parser";
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Extract EXIF operation
+ */
+class ExtractEXIF extends Operation {
+
+    /**
+     * ExtractEXIF constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract EXIF";
+        this.module = "Image";
+        this.description = [
+            "Extracts EXIF data from an image.",
+            "<br><br>",
+            "EXIF data is metadata embedded in images (JPEG, JPG, TIFF) and audio files.",
+            "<br><br>",
+            "EXIF data from photos usually contains information about the image file itself as well as the device used to create it.",
+        ].join("\n");
+        this.inputType = "ArrayBuffer";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {ArrayBuffer} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        try {
+            const parser = ExifParser.create(input);
+            const result = parser.parse();
+
+            const lines = [];
+            for (const tagName in result.tags) {
+                const value = result.tags[tagName];
+                lines.push(`${tagName}: ${value}`);
+            }
+
+            const numTags = lines.length;
+            lines.unshift(`Found ${numTags} tags.\n`);
+            return lines.join("\n");
+        } catch (err) {
+            throw new OperationError(`Could not extract EXIF data from image: ${err}`);
+        }
+    }
+
+}
+
+export default ExtractEXIF;

+ 86 - 0
src/core/operations/FindReplace.mjs

@@ -0,0 +1,86 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Find / Replace operation
+ */
+class FindReplace extends Operation {
+
+    /**
+     * FindReplace constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Find / Replace";
+        this.module = "Regex";
+        this.description = "Replaces all occurrences of the first string with the second.<br><br>Includes support for regular expressions (regex), simple strings and extended strings (which support \\n, \\r, \\t, \\b, \\f and escaped hex bytes using \\x notation, e.g. \\x00 for a null byte).";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Find",
+                "type": "toggleString",
+                "value": "",
+                "toggleValues": ["Regex", "Extended (\\n, \\t, \\x...)", "Simple string"]
+            },
+            {
+                "name": "Replace",
+                "type": "binaryString",
+                "value": ""
+            },
+            {
+                "name": "Global match",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Case insensitive",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Multiline matching",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [{option: type}, replace, g, i, m] = args;
+        let find = args[0].string,
+            modifiers = "";
+
+        if (g) modifiers += "g";
+        if (i) modifiers += "i";
+        if (m) modifiers += "m";
+
+        if (type === "Regex") {
+            find = new RegExp(find, modifiers);
+            return input.replace(find, replace);
+        }
+
+        if (type.indexOf("Extended") === 0) {
+            find = Utils.parseEscapedChars(find);
+        }
+
+        find = new RegExp(Utils.escapeRegex(find), modifiers);
+
+        return input.replace(find, replace);
+    }
+
+}
+
+export default FindReplace;

+ 40 - 0
src/core/operations/HexToObjectIdentifier.mjs

@@ -0,0 +1,40 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation";
+
+/**
+ * Hex to Object Identifier operation
+ */
+class HexToObjectIdentifier extends Operation {
+
+    /**
+     * HexToObjectIdentifier constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Hex to Object Identifier";
+        this.module = "PublicKey";
+        this.description = "Converts a hexadecimal string into an object identifier (OID).";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        return r.KJUR.asn1.ASN1Util.oidHexToInt(input.replace(/\s/g, ""));
+    }
+
+}
+
+export default HexToObjectIdentifier;

+ 46 - 0
src/core/operations/HexToPEM.mjs

@@ -0,0 +1,46 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation";
+
+/**
+ * Hex to PEM operation
+ */
+class HexToPEM extends Operation {
+
+    /**
+     * HexToPEM constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Hex to PEM";
+        this.module = "PublicKey";
+        this.description = "Converts a hexadecimal DER (Distinguished Encoding Rules) string into PEM (Privacy Enhanced Mail) format.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Header string",
+                "type": "string",
+                "value": "CERTIFICATE"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        return r.KJUR.asn1.ASN1Util.getPEMStringFromHex(input.replace(/\s/g, ""), args[0]);
+    }
+
+}
+
+export default HexToPEM;

+ 68 - 0
src/core/operations/JPathExpression.mjs

@@ -0,0 +1,68 @@
+/**
+ * @author Matt C (matt@artemisbot.uk)
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import jpath from "jsonpath";
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+
+/**
+ * JPath expression operation
+ */
+class JPathExpression extends Operation {
+
+    /**
+     * JPathExpression constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "JPath expression";
+        this.module = "Code";
+        this.description = "Extract information from a JSON object with a JPath query.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Query",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Result delimiter",
+                "type": "binaryShortString",
+                "value": "\\n"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [query, delimiter] = args;
+        let results,
+            obj;
+
+        try {
+            obj = JSON.parse(input);
+        } catch (err) {
+            throw new OperationError(`Invalid input JSON: ${err.message}`);
+        }
+
+        try {
+            results = jpath.query(obj, query);
+        } catch (err) {
+            throw new OperationError(`Invalid JPath expression: ${err.message}`);
+        }
+
+        return results.map(result => JSON.stringify(result)).join(delimiter);
+    }
+
+}
+
+export default JPathExpression;

+ 48 - 0
src/core/operations/JSONBeautify.mjs

@@ -0,0 +1,48 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * JSON Beautify operation
+ */
+class JSONBeautify extends Operation {
+
+    /**
+     * JSONBeautify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "JSON Beautify";
+        this.module = "Code";
+        this.description = "Indents and prettifies JavaScript Object Notation (JSON) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Indent string",
+                "type": "binaryShortString",
+                "value": "\\t"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const indentStr = args[0];
+        if (!input) return "";
+        return vkbeautify.json(input, indentStr);
+    }
+
+}
+
+export default JSONBeautify;

+ 41 - 0
src/core/operations/JSONMinify.mjs

@@ -0,0 +1,41 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * JSON Minify operation
+ */
+class JSONMinify extends Operation {
+
+    /**
+     * JSONMinify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "JSON Minify";
+        this.module = "Code";
+        this.description = "Compresses JavaScript Object Notation (JSON) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        if (!input) return "";
+        return vkbeautify.jsonmin(input);
+    }
+
+}
+
+export default JSONMinify;

+ 40 - 0
src/core/operations/ObjectIdentifierToHex.mjs

@@ -0,0 +1,40 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation";
+
+/**
+ * Object Identifier to Hex operation
+ */
+class ObjectIdentifierToHex extends Operation {
+
+    /**
+     * ObjectIdentifierToHex constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Object Identifier to Hex";
+        this.module = "PublicKey";
+        this.description = "Converts an object identifier (OID) into a hexadecimal string.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        return r.KJUR.asn1.ASN1Util.oidIntToHex(input);
+    }
+
+}
+
+export default ObjectIdentifierToHex;

+ 50 - 0
src/core/operations/PEMToHex.mjs

@@ -0,0 +1,50 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation";
+
+/**
+ * PEM to Hex operation
+ */
+class PEMToHex extends Operation {
+
+    /**
+     * PEMToHex constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "PEM to Hex";
+        this.module = "PublicKey";
+        this.description = "Converts PEM (Privacy Enhanced Mail) format to a hexadecimal DER (Distinguished Encoding Rules) string.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        if (input.indexOf("-----BEGIN") < 0) {
+            // Add header so that the KEYUTIL function works
+            input = "-----BEGIN CERTIFICATE-----" + input;
+        }
+        if (input.indexOf("-----END") < 0) {
+            // Add footer so that the KEYUTIL function works
+            input = input + "-----END CERTIFICATE-----";
+        }
+        const cert = new r.X509();
+        cert.readCertPEM(input);
+        return cert.hex;
+    }
+
+}
+
+export default PEMToHex;

+ 54 - 0
src/core/operations/ParseASN1HexString.mjs

@@ -0,0 +1,54 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation";
+
+/**
+ * Parse ASN.1 hex string operation
+ */
+class ParseASN1HexString extends Operation {
+
+    /**
+     * ParseASN1HexString constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Parse ASN.1 hex string";
+        this.module = "PublicKey";
+        this.description = "Abstract Syntax Notation One (ASN.1) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking.<br><br>This operation parses arbitrary ASN.1 data and presents the resulting tree.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Starting index",
+                "type": "number",
+                "value": 0
+            },
+            {
+                "name": "Truncate octet strings longer than",
+                "type": "number",
+                "value": 32
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [index, truncateLen] = args;
+        return r.ASN1HEX.dump(input.replace(/\s/g, ""), {
+            "ommitLongOctet": truncateLen
+        }, index);
+    }
+
+}
+
+export default ParseASN1HexString;

+ 216 - 0
src/core/operations/ParseX509Certificate.mjs

@@ -0,0 +1,216 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import { fromBase64 } from "../lib/Base64";
+import { toHex } from "../lib/Hex";
+import { formatByteStr, formatDnStr } from "../lib/PublicKey";
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Parse X.509 certificate operation
+ */
+class ParseX509Certificate extends Operation {
+
+    /**
+     * ParseX509Certificate constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Parse X.509 certificate";
+        this.module = "PublicKey";
+        this.description = "X.509 is an ITU-T standard for a public key infrastructure (PKI) and Privilege Management Infrastructure (PMI). It is commonly involved with SSL/TLS security.<br><br>This operation displays the contents of a certificate in a human readable format, similar to the openssl command line tool.<br><br>Tags: X509, server hello, handshake";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Input format",
+                "type": "option",
+                "value": ["PEM", "DER Hex", "Base64", "Raw"]
+            }
+        ];
+        this.patterns = [
+            {
+                "match": "^-+BEGIN CERTIFICATE-+\\r?\\n[\\da-z+/\\n\\r]+-+END CERTIFICATE-+\\r?\\n?$",
+                "flags": "i",
+                "args": [
+                    "PEM"
+                ]
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        if (!input.length) {
+            return "No input";
+        }
+
+        const cert = new r.X509(),
+            inputFormat = args[0];
+
+        switch (inputFormat) {
+            case "DER Hex":
+                input = input.replace(/\s/g, "");
+                cert.readCertHex(input);
+                break;
+            case "PEM":
+                cert.readCertPEM(input);
+                break;
+            case "Base64":
+                cert.readCertHex(toHex(fromBase64(input, null, "byteArray"), ""));
+                break;
+            case "Raw":
+                cert.readCertHex(toHex(Utils.strToByteArray(input), ""));
+                break;
+            default:
+                throw "Undefined input format";
+        }
+
+        const sn = cert.getSerialNumberHex(),
+            issuer = cert.getIssuerString(),
+            subject = cert.getSubjectString(),
+            pk = cert.getPublicKey(),
+            pkFields = [],
+            sig = cert.getSignatureValueHex();
+
+        let pkStr = "",
+            sigStr = "",
+            extensions = "";
+
+        // Public Key fields
+        pkFields.push({
+            key: "Algorithm",
+            value: pk.type
+        });
+
+        if (pk.type === "EC") { // ECDSA
+            pkFields.push({
+                key: "Curve Name",
+                value: pk.curveName
+            });
+            pkFields.push({
+                key: "Length",
+                value: (((new r.BigInteger(pk.pubKeyHex, 16)).bitLength()-3) /2) + " bits"
+            });
+            pkFields.push({
+                key: "pub",
+                value: formatByteStr(pk.pubKeyHex, 16, 18)
+            });
+        } else if (pk.type === "DSA") { // DSA
+            pkFields.push({
+                key: "pub",
+                value: formatByteStr(pk.y.toString(16), 16, 18)
+            });
+            pkFields.push({
+                key: "P",
+                value: formatByteStr(pk.p.toString(16), 16, 18)
+            });
+            pkFields.push({
+                key: "Q",
+                value: formatByteStr(pk.q.toString(16), 16, 18)
+            });
+            pkFields.push({
+                key: "G",
+                value: formatByteStr(pk.g.toString(16), 16, 18)
+            });
+        } else if (pk.e) { // RSA
+            pkFields.push({
+                key: "Length",
+                value: pk.n.bitLength() + " bits"
+            });
+            pkFields.push({
+                key: "Modulus",
+                value: formatByteStr(pk.n.toString(16), 16, 18)
+            });
+            pkFields.push({
+                key: "Exponent",
+                value: pk.e + " (0x" + pk.e.toString(16) + ")"
+            });
+        } else {
+            pkFields.push({
+                key: "Error",
+                value: "Unknown Public Key type"
+            });
+        }
+
+        // Format Public Key fields
+        for (let i = 0; i < pkFields.length; i++) {
+            pkStr += `  ${pkFields[i].key}:${(pkFields[i].value + "\n").padStart(
+                18 - (pkFields[i].key.length + 3) + pkFields[i].value.length + 1,
+                " "
+            )}`;
+        }
+
+        // Signature fields
+        let breakoutSig = false;
+        try {
+            breakoutSig = r.ASN1HEX.dump(sig).indexOf("SEQUENCE") === 0;
+        } catch (err) {
+            // Error processing signature, output without further breakout
+        }
+
+        if (breakoutSig) { // DSA or ECDSA
+            sigStr = `  r:              ${formatByteStr(r.ASN1HEX.getV(sig, 4), 16, 18)}
+  s:              ${formatByteStr(r.ASN1HEX.getV(sig, 48), 16, 18)}`;
+        } else { // RSA or unknown
+            sigStr = `  Signature:      ${formatByteStr(sig, 16, 18)}`;
+        }
+
+        // Extensions
+        try {
+            extensions = cert.getInfo().split("X509v3 Extensions:\n")[1].split("signature")[0];
+        } catch (err) {}
+
+        const issuerStr = formatDnStr(issuer, 2),
+            nbDate = formatDate(cert.getNotBefore()),
+            naDate = formatDate(cert.getNotAfter()),
+            subjectStr = formatDnStr(subject, 2);
+
+        return `Version:          ${cert.version} (0x${Utils.hex(cert.version - 1)})
+Serial number:    ${new r.BigInteger(sn, 16).toString()} (0x${sn})
+Algorithm ID:     ${cert.getSignatureAlgorithmField()}
+Validity
+  Not Before:     ${nbDate} (dd-mm-yy hh:mm:ss) (${cert.getNotBefore()})
+  Not After:      ${naDate} (dd-mm-yy hh:mm:ss) (${cert.getNotAfter()})
+Issuer
+${issuerStr}
+Subject
+${subjectStr}
+Public Key
+${pkStr.slice(0, -1)}
+Certificate Signature
+  Algorithm:      ${cert.getSignatureAlgorithmName()}
+${sigStr}
+
+Extensions
+${extensions}`;
+    }
+
+}
+
+/**
+ * Formats dates.
+ *
+ * @param {string} dateStr
+ * @returns {string}
+ */
+function formatDate (dateStr) {
+    return dateStr[4] + dateStr[5] + "/" +
+        dateStr[2] + dateStr[3] + "/" +
+        dateStr[0] + dateStr[1] + " " +
+        dateStr[6] + dateStr[7] + ":" +
+        dateStr[8] + dateStr[9] + ":" +
+        dateStr[10] + dateStr[11];
+}
+
+export default ParseX509Certificate;

+ 262 - 0
src/core/operations/RegularExpression.mjs

@@ -0,0 +1,262 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+
+import XRegExp from "xregexp";
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Regular expression operation
+ */
+class RegularExpression extends Operation {
+
+    /**
+     * RegularExpression constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Regular expression";
+        this.module = "Regex";
+        this.description = "Define your own regular expression (regex) to search the input data with, optionally choosing from a list of pre-defined patterns.<br><br>Supports extended regex syntax including the 'dot matches all' flag, named capture groups, full unicode coverage (including <code>\\p{}</code> categories and scripts as well as astral codes) and recursive matching.";
+        this.inputType = "string";
+        this.outputType = "html";
+        this.args = [
+            {
+                "name": "Built in regexes",
+                "type": "populateOption",
+                "value": [
+                    {
+                        name: "User defined",
+                        value: ""
+                    },
+                    {
+                        name: "IPv4 address",
+                        value: "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?"
+                    },
+                    {
+                        name: "IPv6 address",
+                        value: "((?=.*::)(?!.*::.+::)(::)?([\\dA-Fa-f]{1,4}:(:|\\b)|){5}|([\\dA-Fa-f]{1,4}:){6})((([\\dA-Fa-f]{1,4}((?!\\3)::|:\\b|(?![\\dA-Fa-f])))|(?!\\2\\3)){2}|(((2[0-4]|1\\d|[1-9])?\\d|25[0-5])\\.?\\b){4})"
+                    },
+                    {
+                        name: "Email address",
+                        value: "\\b(\\w[-.\\w]*)@([-\\w]+(?:\\.[-\\w]+)*)\\.([A-Za-z]{2,4})\\b"
+                    },
+                    {
+                        name: "URL",
+                        value: "([A-Za-z]+://)([-\\w]+(?:\\.\\w[-\\w]*)+)(:\\d+)?(/[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]*(?:[.!,?]+[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]+)*)?"
+                    },
+                    {
+                        name: "Domain",
+                        value: "\\b((?=[a-z0-9-]{1,63}\\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,63}\\b"
+                    },
+                    {
+                        name: "Windows file path",
+                        value: "([A-Za-z]):\\\\((?:[A-Za-z\\d][A-Za-z\\d\\- \\x27_\\(\\)~]{0,61}\\\\?)*[A-Za-z\\d][A-Za-z\\d\\- \\x27_\\(\\)]{0,61})(\\.[A-Za-z\\d]{1,6})?"
+                    },
+                    {
+                        name: "UNIX file path",
+                        value: "(?:/[A-Za-z\\d.][A-Za-z\\d\\-.]{0,61})+"
+                    },
+                    {
+                        name: "MAC address",
+                        value: "[A-Fa-f\\d]{2}(?:[:-][A-Fa-f\\d]{2}){5}"
+                    },
+                    {
+                        name: "Date (yyyy-mm-dd)",
+                        value: "((?:19|20)\\d\\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])"
+                    },
+                    {
+                        name: "Date (dd/mm/yyyy)",
+                        value: "(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.]((?:19|20)\\d\\d)"
+                    },
+                    {
+                        name: "Date (mm/dd/yyyy)",
+                        value: "(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.]((?:19|20)\\d\\d)"
+                    },
+                    {
+                        name: "Strings",
+                        value: "[A-Za-z\\d/\\-:.,_$%\\x27\"()<>= !\\[\\]{}@]{4,}"
+                    },
+                ],
+                "target": 1
+            },
+            {
+                "name": "Regex",
+                "type": "text",
+                "value": ""
+            },
+            {
+                "name": "Case insensitive",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "^ and $ match at newlines",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Dot matches all",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Unicode support",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Astral support",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Output format",
+                "type": "option",
+                "value": ["Highlight matches", "List matches", "List capture groups", "List matches with capture groups"]
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {html}
+     */
+    run(input, args) {
+        const [,
+            userRegex,
+            i, m, s, u, a,
+            displayTotal,
+            outputFormat
+        ] = args;
+        let modifiers = "g";
+
+        if (i) modifiers += "i";
+        if (m) modifiers += "m";
+        if (s) modifiers += "s";
+        if (u) modifiers += "u";
+        if (a) modifiers += "A";
+
+        if (userRegex && userRegex !== "^" && userRegex !== "$") {
+            try {
+                const regex = new XRegExp(userRegex, modifiers);
+
+                switch (outputFormat) {
+                    case "Highlight matches":
+                        return regexHighlight(input, regex, displayTotal);
+                    case "List matches":
+                        return Utils.escapeHtml(regexList(input, regex, displayTotal, true, false));
+                    case "List capture groups":
+                        return Utils.escapeHtml(regexList(input, regex, displayTotal, false, true));
+                    case "List matches with capture groups":
+                        return Utils.escapeHtml(regexList(input, regex, displayTotal, true, true));
+                    default:
+                        return "Error: Invalid output format";
+                }
+            } catch (err) {
+                throw new OperationError("Invalid regex. Details: " + err.message);
+            }
+        } else {
+            return Utils.escapeHtml(input);
+        }
+    }
+
+}
+
+/**
+ * Creates a string listing the matches within a string.
+ *
+ * @param {string} input
+ * @param {RegExp} regex
+ * @param {boolean} displayTotal
+ * @param {boolean} matches - Display full match
+ * @param {boolean} captureGroups - Display each of the capture groups separately
+ * @returns {string}
+ */
+function regexList (input, regex, displayTotal, matches, captureGroups) {
+    let output = "",
+        total = 0,
+        match;
+
+    while ((match = regex.exec(input))) {
+        // Moves pointer when an empty string is matched (prevents infinite loop)
+        if (match.index === regex.lastIndex) {
+            regex.lastIndex++;
+        }
+
+        total++;
+        if (matches) {
+            output += match[0] + "\n";
+        }
+        if (captureGroups) {
+            for (let i = 1; i < match.length; i++) {
+                if (matches) {
+                    output += "  Group " + i + ": ";
+                }
+                output += match[i] + "\n";
+            }
+        }
+    }
+
+    if (displayTotal)
+        output = "Total found: " + total + "\n\n" + output;
+
+    return output.slice(0, -1);
+}
+
+/**
+ * Adds HTML highlights to matches within a string.
+ *
+ * @private
+ * @param {string} input
+ * @param {RegExp} regex
+ * @param {boolean} displayTotal
+ * @returns {string}
+ */
+function regexHighlight (input, regex, displayTotal) {
+    let output = "",
+        m,
+        hl = 1,
+        i = 0,
+        total = 0;
+
+    while ((m = regex.exec(input))) {
+        // Moves pointer when an empty string is matched (prevents infinite loop)
+        if (m.index === regex.lastIndex) {
+            regex.lastIndex++;
+        }
+
+        // Add up to match
+        output += Utils.escapeHtml(input.slice(i, m.index));
+
+        // Add match with highlighting
+        output += "<span class='hl"+hl+"'>" + Utils.escapeHtml(m[0]) + "</span>";
+
+        // Switch highlight
+        hl = hl === 1 ? 2 : 1;
+
+        i = regex.lastIndex;
+        total++;
+    }
+
+    // Add all after final match
+    output += Utils.escapeHtml(input.slice(i, input.length));
+
+    if (displayTotal)
+        output = "Total found: " + total + "\n\n" + output;
+
+    return output;
+}
+
+export default RegularExpression;

+ 54 - 0
src/core/operations/RemoveEXIF.mjs

@@ -0,0 +1,54 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import { removeEXIF } from "../vendor/remove-exif";
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Remove EXIF operation
+ */
+class RemoveEXIF extends Operation {
+
+    /**
+     * RemoveEXIF constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Remove EXIF";
+        this.module = "Image";
+        this.description = [
+            "Removes EXIF data from a JPEG image.",
+            "<br><br>",
+            "EXIF data embedded in photos usually contains information about the image file itself as well as the device used to create it.",
+        ].join("\n");
+        this.inputType = "byteArray";
+        this.outputType = "byteArray";
+        this.args = [];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        // Do nothing if input is empty
+        if (input.length === 0) return input;
+
+        try {
+            return removeEXIF(input);
+        } catch (err) {
+            // Simply return input if no EXIF data is found
+            if (err === "Exif not found.") return input;
+            throw new OperationError(`Could not remove EXIF data from image: ${err}`);
+        }
+    }
+
+}
+
+export default RemoveEXIF;

+ 90 - 0
src/core/operations/RenderImage.mjs

@@ -0,0 +1,90 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import { fromBase64, toBase64 } from "../lib/Base64";
+import { fromHex } from "../lib/Hex";
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import Utils from "../Utils";
+import Magic from "../lib/Magic";
+
+/**
+ * Render Image operation
+ */
+class RenderImage extends Operation {
+
+    /**
+     * RenderImage constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Render Image";
+        this.module = "Image";
+        this.description = "Displays the input as an image. Supports the following formats:<br><br><ul><li>jpg/jpeg</li><li>png</li><li>gif</li><li>webp</li><li>bmp</li><li>ico</li></ul>";
+        this.inputType = "string";
+        this.outputType = "html";
+        this.args = [
+            {
+                "name": "Input format",
+                "type": "option",
+                "value": ["Raw", "Base64", "Hex"]
+            }
+        ];
+        this.patterns = [
+            {
+                "match": "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)",
+                "flags": "",
+                "args": ["Raw"],
+                "useful": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {html}
+     */
+    run(input, args) {
+        const inputFormat = args[0];
+        let dataURI = "data:";
+
+        if (!input.length) return "";
+
+        // Convert input to raw bytes
+        switch (inputFormat) {
+            case "Hex":
+                input = fromHex(input);
+                break;
+            case "Base64":
+                // Don't trust the Base64 entered by the user.
+                // Unwrap it first, then re-encode later.
+                input = fromBase64(input, undefined, "byteArray");
+                break;
+            case "Raw":
+            default:
+                input = Utils.strToByteArray(input);
+                break;
+        }
+
+        // Determine file type
+        const type = Magic.magicFileType(input);
+        if (type && type.mime.indexOf("image") === 0) {
+            dataURI += type.mime + ";";
+        } else {
+            throw new OperationError("Invalid file type");
+        }
+
+        // Add image data to URI
+        dataURI += "base64," + toBase64(input);
+
+        return "<img src='" + dataURI + "'>";
+    }
+
+}
+
+export default RenderImage;

+ 47 - 0
src/core/operations/SQLBeautify.mjs

@@ -0,0 +1,47 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * SQL Beautify operation
+ */
+class SQLBeautify extends Operation {
+
+    /**
+     * SQLBeautify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "SQL Beautify";
+        this.module = "Code";
+        this.description = "Indents and prettifies Structured Query Language (SQL) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Indent string",
+                "type": "binaryShortString",
+                "value": "\\t"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const indentStr = args[0];
+        return vkbeautify.sql(input, indentStr);
+    }
+
+}
+
+export default SQLBeautify;

+ 40 - 0
src/core/operations/SQLMinify.mjs

@@ -0,0 +1,40 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * SQL Minify operation
+ */
+class SQLMinify extends Operation {
+
+    /**
+     * SQLMinify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "SQL Minify";
+        this.module = "Code";
+        this.description = "Compresses Structured Query Language (SQL) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        return vkbeautify.sqlmin(input);
+    }
+
+}
+
+export default SQLMinify;

+ 47 - 0
src/core/operations/XMLBeautify.mjs

@@ -0,0 +1,47 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * XML Beautify operation
+ */
+class XMLBeautify extends Operation {
+
+    /**
+     * XMLBeautify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "XML Beautify";
+        this.module = "Code";
+        this.description = "Indents and prettifies eXtensible Markup Language (XML) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Indent string",
+                "type": "binaryShortString",
+                "value": "\\t"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const indentStr = args[0];
+        return vkbeautify.xml(input, indentStr);
+    }
+
+}
+
+export default XMLBeautify;

+ 47 - 0
src/core/operations/XMLMinify.mjs

@@ -0,0 +1,47 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import vkbeautify from "vkbeautify";
+import Operation from "../Operation";
+
+/**
+ * XML Minify operation
+ */
+class XMLMinify extends Operation {
+
+    /**
+     * XMLMinify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "XML Minify";
+        this.module = "Code";
+        this.description = "Compresses eXtensible Markup Language (XML) code.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Preserve comments",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const preserveComments = args[0];
+        return vkbeautify.xmlmin(input, preserveComments);
+    }
+
+}
+
+export default XMLMinify;

+ 73 - 0
src/core/operations/XPathExpression.mjs

@@ -0,0 +1,73 @@
+/**
+ * @author Mikescher (https://github.com/Mikescher | https://mikescher.com)
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import xmldom from "xmldom";
+import xpath from "xpath";
+
+/**
+ * XPath expression operation
+ */
+class XPathExpression extends Operation {
+
+    /**
+     * XPathExpression constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "XPath expression";
+        this.module = "Code";
+        this.description = "Extract information from an XML document with an XPath query";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "XPath",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Result delimiter",
+                "type": "binaryShortString",
+                "value": "\\n"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [query, delimiter] = args;
+
+        let doc;
+        try {
+            doc = new xmldom.DOMParser().parseFromString(input, "application/xml");
+        } catch (err) {
+            throw new OperationError("Invalid input XML.");
+        }
+
+        let nodes;
+        try {
+            nodes = xpath.select(query, doc);
+        } catch (err) {
+            throw new OperationError(`Invalid XPath. Details:\n${err.message}.`);
+        }
+
+        const nodeToString = function(node) {
+            return node.toString();
+        };
+
+        return nodes.map(nodeToString).join(delimiter);
+    }
+
+}
+
+export default XPathExpression;

+ 0 - 540
src/core/operations/legacy/Code.js

@@ -1,540 +0,0 @@
-import {camelCase, kebabCase, snakeCase} from "lodash";
-import vkbeautify from "vkbeautify";
-import {DOMParser} from "xmldom";
-import xpath from "xpath";
-import jpath from "jsonpath";
-import nwmatcher from "nwmatcher";
-import hljs from "highlight.js";
-
-
-/**
- * Code operations.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @namespace
- */
-const Code = {
-
-    /**
-     * @constant
-     * @default
-     */
-    LANGUAGES: ["auto detect"].concat(hljs.listLanguages()),
-
-    /**
-     * Syntax highlighter operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {html}
-     */
-    runSyntaxHighlight: function(input, args) {
-        const language = args[0];
-
-        if (language === "auto detect") {
-            return hljs.highlightAuto(input).value;
-        }
-
-        return hljs.highlight(language, input, true).value;
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    BEAUTIFY_INDENT: "\\t",
-
-    /**
-     * XML Beautify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runXmlBeautify: function(input, args) {
-        const indentStr = args[0];
-        return vkbeautify.xml(input, indentStr);
-    },
-
-
-    /**
-     * JSON Beautify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runJsonBeautify: function(input, args) {
-        const indentStr = args[0];
-        if (!input) return "";
-        return vkbeautify.json(input, indentStr);
-    },
-
-
-    /**
-     * CSS Beautify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runCssBeautify: function(input, args) {
-        const indentStr = args[0];
-        return vkbeautify.css(input, indentStr);
-    },
-
-
-    /**
-     * SQL Beautify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runSqlBeautify: function(input, args) {
-        const indentStr = args[0];
-        return vkbeautify.sql(input, indentStr);
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    PRESERVE_COMMENTS: false,
-
-    /**
-     * XML Minify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runXmlMinify: function(input, args) {
-        const preserveComments = args[0];
-        return vkbeautify.xmlmin(input, preserveComments);
-    },
-
-
-    /**
-     * JSON Minify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runJsonMinify: function(input, args) {
-        if (!input) return "";
-        return vkbeautify.jsonmin(input);
-    },
-
-
-    /**
-     * CSS Minify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runCssMinify: function(input, args) {
-        const preserveComments = args[0];
-        return vkbeautify.cssmin(input, preserveComments);
-    },
-
-
-    /**
-     * SQL Minify operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runSqlMinify: function(input, args) {
-        return vkbeautify.sqlmin(input);
-    },
-
-
-    /**
-     * Generic Code Beautify operation.
-     *
-     * Yeeeaaah...
-     *
-     * I'm not proud of this code, but seriously, try writing a generic lexer and parser that
-     * correctly generates an AST for multiple different languages. I have tried, and I can tell
-     * you it's pretty much impossible.
-     *
-     * This basically works. That'll have to be good enough. It's not meant to produce working code,
-     * just slightly more readable code.
-     *
-     * Things that don't work:
-     *  - For loop formatting
-     *  - Do-While loop formatting
-     *  - Switch/Case indentation
-     *  - Bit shift operators
-     *
-     * @author n1474335 [n1474335@gmail.com]
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runGenericBeautify: function(input, args) {
-        let code = input,
-            t = 0,
-            preservedTokens = [],
-            m;
-
-        // Remove strings
-        const sstrings = /'([^'\\]|\\.)*'/g;
-        while ((m = sstrings.exec(code))) {
-            code = preserveToken(code, m, t++);
-            sstrings.lastIndex = m.index;
-        }
-
-        const dstrings = /"([^"\\]|\\.)*"/g;
-        while ((m = dstrings.exec(code))) {
-            code = preserveToken(code, m, t++);
-            dstrings.lastIndex = m.index;
-        }
-
-        // Remove comments
-        const scomments = /\/\/[^\n\r]*/g;
-        while ((m = scomments.exec(code))) {
-            code = preserveToken(code, m, t++);
-            scomments.lastIndex = m.index;
-        }
-
-        const mcomments = /\/\*[\s\S]*?\*\//gm;
-        while ((m = mcomments.exec(code))) {
-            code = preserveToken(code, m, t++);
-            mcomments.lastIndex = m.index;
-        }
-
-        const hcomments = /(^|\n)#[^\n\r#]+/g;
-        while ((m = hcomments.exec(code))) {
-            code = preserveToken(code, m, t++);
-            hcomments.lastIndex = m.index;
-        }
-
-        // Remove regexes
-        const regexes = /\/.*?[^\\]\/[gim]{0,3}/gi;
-        while ((m = regexes.exec(code))) {
-            code = preserveToken(code, m, t++);
-            regexes.lastIndex = m.index;
-        }
-
-        code = code
-            // Create newlines after ;
-            .replace(/;/g, ";\n")
-            // Create newlines after { and around }
-            .replace(/{/g, "{\n")
-            .replace(/}/g, "\n}\n")
-            // Remove carriage returns
-            .replace(/\r/g, "")
-            // Remove all indentation
-            .replace(/^\s+/g, "")
-            .replace(/\n\s+/g, "\n")
-            // Remove trailing spaces
-            .replace(/\s*$/g, "")
-            .replace(/\n{/g, "{");
-
-        // Indent
-        let i = 0,
-            level = 0,
-            indent;
-        while (i < code.length) {
-            switch (code[i]) {
-                case "{":
-                    level++;
-                    break;
-                case "\n":
-                    if (i+1 >= code.length) break;
-
-                    if (code[i+1] === "}") level--;
-                    indent = (level >= 0) ? Array(level*4+1).join(" ") : "";
-
-                    code = code.substring(0, i+1) + indent + code.substring(i+1);
-                    if (level > 0) i += level*4;
-                    break;
-            }
-            i++;
-        }
-
-        code = code
-            // Add strategic spaces
-            .replace(/\s*([!<>=+-/*]?)=\s*/g, " $1= ")
-            .replace(/\s*<([=]?)\s*/g, " <$1 ")
-            .replace(/\s*>([=]?)\s*/g, " >$1 ")
-            .replace(/([^+])\+([^+=])/g, "$1 + $2")
-            .replace(/([^-])-([^-=])/g, "$1 - $2")
-            .replace(/([^*])\*([^*=])/g, "$1 * $2")
-            .replace(/([^/])\/([^/=])/g, "$1 / $2")
-            .replace(/\s*,\s*/g, ", ")
-            .replace(/\s*{/g, " {")
-            .replace(/}\n/g, "}\n\n")
-            // Hacky horribleness
-            .replace(/(if|for|while|with|elif|elseif)\s*\(([^\n]*)\)\s*\n([^{])/gim, "$1 ($2)\n    $3")
-            .replace(/(if|for|while|with|elif|elseif)\s*\(([^\n]*)\)([^{])/gim, "$1 ($2) $3")
-            .replace(/else\s*\n([^{])/gim, "else\n    $1")
-            .replace(/else\s+([^{])/gim, "else $1")
-            // Remove strategic spaces
-            .replace(/\s+;/g, ";")
-            .replace(/\{\s+\}/g, "{}")
-            .replace(/\[\s+\]/g, "[]")
-            .replace(/}\s*(else|catch|except|finally|elif|elseif|else if)/gi, "} $1");
-
-        // Replace preserved tokens
-        const ptokens = /###preservedToken(\d+)###/g;
-        while ((m = ptokens.exec(code))) {
-            const ti = parseInt(m[1], 10);
-            code = code.substring(0, m.index) + preservedTokens[ti] + code.substring(m.index + m[0].length);
-            ptokens.lastIndex = m.index;
-        }
-
-        return code;
-
-        /**
-         * Replaces a matched token with a placeholder value.
-         */
-        function preserveToken(str, match, t) {
-            preservedTokens[t] = match[0];
-            return str.substring(0, match.index) +
-                "###preservedToken" + t + "###" +
-                str.substring(match.index + match[0].length);
-        }
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    XPATH_INITIAL: "",
-
-    /**
-     * @constant
-     * @default
-     */
-    XPATH_DELIMITER: "\\n",
-
-    /**
-     * XPath expression operation.
-     *
-     * @author Mikescher (https://github.com/Mikescher | https://mikescher.com)
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runXpath: function(input, args) {
-        let query = args[0],
-            delimiter = args[1];
-
-        let doc;
-        try {
-            doc = new DOMParser().parseFromString(input, "application/xml");
-        } catch (err) {
-            return "Invalid input XML.";
-        }
-
-        let nodes;
-        try {
-            nodes = xpath.select(query, doc);
-        } catch (err) {
-            return "Invalid XPath. Details:\n" + err.message;
-        }
-
-        const nodeToString = function(node) {
-            return node.toString();
-        };
-
-        return nodes.map(nodeToString).join(delimiter);
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    JPATH_INITIAL: "",
-
-    /**
-     * @constant
-     * @default
-     */
-    JPATH_DELIMITER: "\\n",
-
-    /**
-     * JPath expression operation.
-     *
-     * @author Matt C (matt@artemisbot.uk)
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runJpath: function(input, args) {
-        let query = args[0],
-            delimiter = args[1],
-            results,
-            obj;
-
-        try {
-            obj = JSON.parse(input);
-        } catch (err) {
-            return "Invalid input JSON: " + err.message;
-        }
-
-        try {
-            results = jpath.query(obj, query);
-        } catch (err) {
-            return "Invalid JPath expression: " + err.message;
-        }
-
-        return results.map(result => JSON.stringify(result)).join(delimiter);
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    CSS_SELECTOR_INITIAL: "",
-
-    /**
-     * @constant
-     * @default
-     */
-    CSS_QUERY_DELIMITER: "\\n",
-
-    /**
-     * CSS selector operation.
-     *
-     * @author Mikescher (https://github.com/Mikescher | https://mikescher.com)
-     * @author n1474335 [n1474335@gmail.com]
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runCSSQuery: function(input, args) {
-        let query = args[0],
-            delimiter = args[1],
-            parser = new DOMParser(),
-            dom,
-            result;
-
-        if (!query.length || !input.length) {
-            return "";
-        }
-
-        try {
-            dom = parser.parseFromString(input);
-        } catch (err) {
-            return "Invalid input HTML.";
-        }
-
-        try {
-            const matcher = nwmatcher({document: dom});
-            result = matcher.select(query, dom);
-        } catch (err) {
-            return "Invalid CSS Selector. Details:\n" + err.message;
-        }
-
-        const nodeToString = function(node) {
-            return node.toString();
-            /* xmldom does not return the outerHTML value.
-            switch (node.nodeType) {
-                case node.ELEMENT_NODE: return node.outerHTML;
-                case node.ATTRIBUTE_NODE: return node.value;
-                case node.TEXT_NODE: return node.wholeText;
-                case node.COMMENT_NODE: return node.data;
-                case node.DOCUMENT_NODE: return node.outerHTML;
-                default: throw new Error("Unknown Node Type: " + node.nodeType);
-            }*/
-        };
-
-        return result
-            .map(nodeToString)
-            .join(delimiter);
-    },
-
-    /**
-     * This tries to rename variable names in a code snippet according to a function.
-     *
-     * @param {string} input
-     * @param {function} replacer - this function will be fed the token which should be renamed.
-     * @returns {string}
-     */
-    _replaceVariableNames(input, replacer) {
-        const tokenRegex = /\\"|"(?:\\"|[^"])*"|(\b[a-z0-9\-_]+\b)/ig;
-
-        return input.replace(tokenRegex, (...args) => {
-            let match = args[0],
-                quotes = args[1];
-
-            if (!quotes) {
-                return match;
-            } else {
-                return replacer(match);
-            }
-        });
-    },
-
-
-    /**
-     * To Snake Case operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runToSnakeCase(input, args) {
-        const smart = args[0];
-
-        if (smart) {
-            return Code._replaceVariableNames(input, snakeCase);
-        } else {
-            return snakeCase(input);
-        }
-    },
-
-
-    /**
-     * To Camel Case operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runToCamelCase(input, args) {
-        const smart = args[0];
-
-        if (smart) {
-            return Code._replaceVariableNames(input, camelCase);
-        } else {
-            return camelCase(input);
-        }
-    },
-
-
-    /**
-     * To Kebab Case operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runToKebabCase(input, args) {
-        const smart = args[0];
-
-        if (smart) {
-            return Code._replaceVariableNames(input, kebabCase);
-        } else {
-            return kebabCase(input);
-        }
-    },
-
-};
-
-export default Code;

+ 0 - 125
src/core/operations/legacy/Image.js

@@ -1,125 +0,0 @@
-import * as ExifParser from "exif-parser";
-import removeEXIF from "../vendor/remove-exif.js";
-import Utils from "../Utils.js";
-import FileType from "./FileType.js";
-import {fromBase64, toBase64} from "../lib/Base64";
-import {fromHex} from "../lib/Hex";
-
-
-/**
- * Image operations.
- *
- * @author tlwr [toby@toby.codes]
- * @copyright Crown Copyright 2017
- * @license Apache-2.0
- *
- * @namespace
- */
-const Image = {
-
-    /**
-     * Extract EXIF operation.
-     *
-     * Extracts EXIF data from a byteArray, representing a JPG or a TIFF image.
-     *
-     * @param {ArrayBuffer} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runExtractEXIF(input, args) {
-        try {
-            const parser = ExifParser.create(input);
-            const result = parser.parse();
-
-            let lines = [];
-            for (let tagName in result.tags) {
-                let value = result.tags[tagName];
-                lines.push(`${tagName}: ${value}`);
-            }
-
-            const numTags = lines.length;
-            lines.unshift(`Found ${numTags} tags.\n`);
-            return lines.join("\n");
-        } catch (err) {
-            throw "Could not extract EXIF data from image: " + err;
-        }
-    },
-
-
-    /**
-     * Remove EXIF operation.
-     *
-     * Removes EXIF data from a byteArray, representing a JPG.
-     *
-     * @author David Moodie [davidmoodie12@gmail.com]
-     * @param {byteArray} input
-     * @param {Object[]} args
-     * @returns {byteArray}
-     */
-    runRemoveEXIF(input, args) {
-        // Do nothing if input is empty
-        if (input.length === 0) return input;
-
-        try {
-            return removeEXIF(input);
-        } catch (err) {
-            // Simply return input if no EXIF data is found
-            if (err === "Exif not found.") return input;
-            throw "Could not remove EXIF data from image: " + err;
-        }
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    INPUT_FORMAT: ["Raw", "Base64", "Hex"],
-
-    /**
-     * Render Image operation.
-     *
-     * @author n1474335 [n1474335@gmail.com]
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {html}
-     */
-    runRenderImage(input, args) {
-        const inputFormat = args[0];
-        let dataURI = "data:";
-
-        if (!input.length) return "";
-
-        // Convert input to raw bytes
-        switch (inputFormat) {
-            case "Hex":
-                input = fromHex(input);
-                break;
-            case "Base64":
-                // Don't trust the Base64 entered by the user.
-                // Unwrap it first, then re-encode later.
-                input = fromBase64(input, null, "byteArray");
-                break;
-            case "Raw":
-            default:
-                input = Utils.strToByteArray(input);
-                break;
-        }
-
-        // Determine file type
-        const type = FileType.magicType(input);
-        if (type && type.mime.indexOf("image") === 0) {
-            dataURI += type.mime + ";";
-        } else {
-            throw "Invalid file type";
-        }
-
-        // Add image data to URI
-        dataURI += "base64," + toBase64(input);
-
-        return "<img src='" + dataURI + "'>";
-    },
-
-};
-
-export default Image;

+ 2 - 4
src/core/vendor/remove-exif.js → src/core/vendor/remove-exif.mjs

@@ -18,10 +18,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 */
 
-import Utils from "../Utils.js";
+import Utils from "../Utils.mjs";
 
 // Param jpeg should be a binaryArray
-function removeEXIF(jpeg) {
+export function removeEXIF(jpeg) {
   // Convert binaryArray to char string
   jpeg = Utils.byteArrayToChars(jpeg);
   if (jpeg.slice(0, 2) != "\xff\xd8") {
@@ -149,5 +149,3 @@ function unpack(mark, str) {
 
   return unpacked;
 }
-
-export default removeEXIF;

+ 4 - 4
test/index.mjs

@@ -34,7 +34,7 @@ import "./tests/operations/CartesianProduct";
 import "./tests/operations/CharEnc";
 import "./tests/operations/Ciphers";
 import "./tests/operations/Checksum";
-// import "./tests/operations/Code";
+import "./tests/operations/Code";
 import "./tests/operations/Compress";
 import "./tests/operations/Crypt";
 import "./tests/operations/DateTime";
@@ -45,16 +45,16 @@ import "./tests/operations/Register";
 import "./tests/operations/Comment";
 import "./tests/operations/Hash";
 import "./tests/operations/Hexdump";
-// import "./tests/operations/Image";
+import "./tests/operations/Image";
 import "./tests/operations/MorseCode";
 import "./tests/operations/MS";
 import "./tests/operations/PHP";
 import "./tests/operations/NetBIOS";
 import "./tests/operations/OTP";
 import "./tests/operations/PowerSet";
-// import "./tests/operations/Regex";
+import "./tests/operations/Regex";
 import "./tests/operations/Rotate";
-// import "./tests/operations/StrUtils";
+import "./tests/operations/StrUtils";
 import "./tests/operations/SeqUtils";
 import "./tests/operations/SetDifference";
 import "./tests/operations/SetIntersection";

+ 2 - 2
test/tests/operations/Image.mjs

@@ -57,7 +57,7 @@ TestRegister.addTests([
     {
         name: "Extract EXIF: hello world text (error)",
         input: "hello world",
-        expectedError: true,
+        expectedOutput: "Could not extract EXIF data from image: Error: Invalid JPEG section offset",
         recipeConfig: [
             {
                 op: "Extract EXIF",
@@ -129,7 +129,7 @@ TestRegister.addTests([
     {
         name: "Remove EXIF: hello world text (error)",
         input: "hello world",
-        expectedError: true,
+        expectedOutput: "Could not remove EXIF data from image: Given data is not jpeg.",
         recipeConfig: [
             {
                 op: "Remove EXIF",

Some files were not shown because too many files changed in this diff