Browse Source

Merge branch 'artemisbot-esm' into esm

n1474335 7 years ago
parent
commit
ae55fde591

+ 10 - 10
src/core/config/Categories.js

@@ -84,8 +84,8 @@ const Categories = [
     //         "RC2 Decrypt",
     //         "RC4",
     //         "RC4 Drop",
-    //         "ROT13",
-    //         "ROT47",
+            "ROT13",
+            "ROT47",
     //         "XOR",
     //         "XOR Brute Force",
     //         "Vigenère Encode",
@@ -116,9 +116,9 @@ const Categories = [
     //         "Object Identifier to Hex",
     //     ]
     // },
-    // {
-    //     name: "Arithmetic / Logic",
-    //     ops: [
+    {
+        name: "Arithmetic / Logic",
+        ops: [
     //         "XOR",
     //         "XOR Brute Force",
     //         "OR",
@@ -135,11 +135,11 @@ const Categories = [
     //         "Standard Deviation",
     //         "Bit shift left",
     //         "Bit shift right",
-    //         "Rotate left",
-    //         "Rotate right",
-    //         "ROT13",
-    //     ]
-    // },
+            "Rotate left",
+            "Rotate right",
+            "ROT13"
+        ]
+    },
     // {
     //     name: "Networking",
     //     ops: [

+ 77 - 1
src/core/config/OperationConfig.json

@@ -155,6 +155,44 @@
             }
         ]
     },
+    "ROT13": {
+        "module": "Default",
+        "description": "A simple caesar substitution cipher which rotates alphabet characters by the specified amount (default 13).",
+        "inputType": "byteArray",
+        "outputType": "byteArray",
+        "flowControl": false,
+        "args": [
+            {
+                "name": "Rotate lower case chars",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Rotate upper case chars",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Amount",
+                "type": "number",
+                "value": 13
+            }
+        ]
+    },
+    "ROT47": {
+        "module": "Default",
+        "description": "A slightly more complex variation of a caesar cipher, which includes ASCII characters from 33 '!' to 126 '~'. Default rotation: 47.",
+        "inputType": "byteArray",
+        "outputType": "byteArray",
+        "flowControl": false,
+        "args": [
+            {
+                "name": "Amount",
+                "type": "number",
+                "value": 47
+            }
+        ]
+    },
     "Raw Deflate": {
         "module": "Compression",
         "description": "Compresses data using the deflate algorithm with no headers.",
@@ -210,6 +248,44 @@
             }
         ]
     },
+    "Rotate left": {
+        "module": "Default",
+        "description": "Rotates each byte to the left by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
+        "inputType": "byteArray",
+        "outputType": "byteArray",
+        "flowControl": false,
+        "args": [
+            {
+                "name": "Amount",
+                "type": "number",
+                "value": 1
+            },
+            {
+                "name": "Carry through",
+                "type": "boolean",
+                "value": false
+            }
+        ]
+    },
+    "Rotate right": {
+        "module": "Default",
+        "description": "Rotates each byte to the right by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
+        "inputType": "byteArray",
+        "outputType": "byteArray",
+        "flowControl": false,
+        "args": [
+            {
+                "name": "Amount",
+                "type": "number",
+                "value": 1
+            },
+            {
+                "name": "Carry through",
+                "type": "boolean",
+                "value": false
+            }
+        ]
+    },
     "Show Base64 offsets": {
         "module": "Default",
         "description": "When a string is within a block of data and the whole block is Base64'd, the string itself could be represented in Base64 in three distinct ways depending on its offset within the block.<br><br>This operation shows all possible offsets for a given string so that each possible encoding can be considered.",
@@ -338,7 +414,7 @@
         "module": "Compression",
         "description": "Decompresses data using the PKZIP algorithm and displays it per file, with support for passwords.",
         "inputType": "byteArray",
-        "outputType": "byteArray",
+        "outputType": "html",
         "flowControl": false,
         "args": [
             {

+ 8 - 0
src/core/config/modules/Default.mjs

@@ -8,6 +8,10 @@
 import FromBase32 from "../../operations/FromBase32";
 import FromBase64 from "../../operations/FromBase64";
 import FromHex from "../../operations/FromHex";
+import ROT13 from "../../operations/ROT13";
+import ROT47 from "../../operations/ROT47";
+import RotateLeft from "../../operations/RotateLeft";
+import RotateRight from "../../operations/RotateRight";
 import ShowBase64Offsets from "../../operations/ShowBase64Offsets";
 import ToBase32 from "../../operations/ToBase32";
 import ToBase64 from "../../operations/ToBase64";
@@ -19,6 +23,10 @@ OpModules.Default = {
     "From Base32": FromBase32,
     "From Base64": FromBase64,
     "From Hex": FromHex,
+    "ROT13": ROT13,
+    "ROT47": ROT47,
+    "Rotate left": RotateLeft,
+    "Rotate right": RotateRight,
     "Show Base64 offsets": ShowBase64Offsets,
     "To Base32": ToBase32,
     "To Base64": ToBase64,

+ 103 - 0
src/core/lib/Rotate.mjs

@@ -0,0 +1,103 @@
+/**
+ * Bit rotation functions.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ *
+ * @todo Support for UTF16
+ */
+
+
+/**
+ * Runs rotation operations across the input data.
+ *
+ * @param {byteArray} data
+ * @param {number} amount
+ * @param {function} algo - The rotation operation to carry out
+ * @returns {byteArray}
+ */
+export function rot(data, amount, algo) {
+    const result = [];
+    for (let i = 0; i < data.length; i++) {
+        let b = data[i];
+        for (let j = 0; j < amount; j++) {
+            b = algo(b);
+        }
+        result.push(b);
+    }
+    return result;
+}
+
+
+/**
+ * Rotate right bitwise op.
+ *
+ * @param {byte} b
+ * @returns {byte}
+ */
+export function rotr(b) {
+    const bit = (b & 1) << 7;
+    return (b >> 1) | bit;
+}
+
+/**
+ * Rotate left bitwise op.
+ *
+ * @param {byte} b
+ * @returns {byte}
+ */
+export function rotl(b) {
+    const bit = (b >> 7) & 1;
+    return ((b << 1) | bit) & 0xFF;
+}
+
+
+/**
+ * Rotates a byte array to the right by a specific amount as a whole, so that bits are wrapped
+ * from the end of the array to the beginning.
+ *
+ * @param {byteArray} data
+ * @param {number} amount
+ * @returns {byteArray}
+ */
+export function rotrCarry(data, amount) {
+    const result = [];
+    let carryBits = 0,
+        newByte;
+
+    amount = amount % 8;
+    for (let i = 0; i < data.length; i++) {
+        const oldByte = data[i] >>> 0;
+        newByte = (oldByte >> amount) | carryBits;
+        carryBits = (oldByte & (Math.pow(2, amount)-1)) << (8-amount);
+        result.push(newByte);
+    }
+    result[0] |= carryBits;
+    return result;
+}
+
+
+/**
+ * Rotates a byte array to the left by a specific amount as a whole, so that bits are wrapped
+ * from the beginning of the array to the end.
+ *
+ * @param {byteArray} data
+ * @param {number} amount
+ * @returns {byteArray}
+ */
+export function rotlCarry(data, amount) {
+    const result = [];
+    let carryBits = 0,
+        newByte;
+
+    amount = amount % 8;
+    for (let i = data.length-1; i >= 0; i--) {
+        const oldByte = data[i];
+        newByte = ((oldByte << amount) | carryBits) & 0xFF;
+        carryBits = (oldByte >> (8-amount)) & (Math.pow(2, amount)-1);
+        result[i] = (newByte);
+    }
+    result[data.length-1] = result[data.length-1] | carryBits;
+    return result;
+}

+ 103 - 0
src/core/operations/ROT13.mjs

@@ -0,0 +1,103 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+
+/**
+ * ROT13 operation.
+ */
+class ROT13 extends Operation {
+
+    /**
+     * ROT13 constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "ROT13";
+        this.module = "Default";
+        this.description = "A simple caesar substitution cipher which rotates alphabet characters by the specified amount (default 13).";
+        this.inputType = "byteArray";
+        this.outputType = "byteArray";
+        this.args = [
+            {
+                name: "Rotate lower case chars",
+                type: "boolean",
+                value: true
+            },
+            {
+                name: "Rotate upper case chars",
+                type: "boolean",
+                value: true
+            },
+            {
+                name: "Amount",
+                type: "number",
+                value: 13
+            },
+        ];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        const output = input,
+            rot13Lowercase = args[0],
+            rot13Upperacse = args[1];
+        let amount = args[2],
+            chr;
+
+        if (amount) {
+            if (amount < 0) {
+                amount = 26 - (Math.abs(amount) % 26);
+            }
+
+            for (let i = 0; i < input.length; i++) {
+                chr = input[i];
+                if (rot13Upperacse && chr >= 65 && chr <= 90) { // Upper case
+                    chr = (chr - 65 + amount) % 26;
+                    output[i] = chr + 65;
+                } else if (rot13Lowercase && chr >= 97 && chr <= 122) { // Lower case
+                    chr = (chr - 97 + amount) % 26;
+                    output[i] = chr + 97;
+                }
+            }
+        }
+        return output;
+    }
+
+    /**
+     * Highlight ROT13
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlight(pos, args) {
+        return pos;
+    }
+
+    /**
+     * Highlight ROT13 in reverse
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlightReverse(pos, args) {
+        return pos;
+    }
+}
+
+export default ROT13;

+ 88 - 0
src/core/operations/ROT47.mjs

@@ -0,0 +1,88 @@
+/**
+ * @author Matt C [matt@artemisbot.uk]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+
+/**
+ * ROT47 operation.
+ */
+class ROT47 extends Operation {
+
+    /**
+     * ROT47 constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "ROT47";
+        this.module = "Default";
+        this.description = "A slightly more complex variation of a caesar cipher, which includes ASCII characters from 33 '!' to 126 '~'. Default rotation: 47.";
+        this.inputType = "byteArray";
+        this.outputType = "byteArray";
+        this.args = [
+            {
+                name: "Amount",
+                type: "number",
+                value: 47
+            },
+        ];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        const output = input;
+        let amount = args[0],
+            chr;
+
+        if (amount) {
+            if (amount < 0) {
+                amount = 94 - (Math.abs(amount) % 94);
+            }
+
+            for (let i = 0; i < input.length; i++) {
+                chr = input[i];
+                if (chr >= 33 && chr <= 126) {
+                    chr = (chr - 33 + amount) % 94;
+                    output[i] = chr + 33;
+                }
+            }
+        }
+        return output;
+    }
+
+    /**
+     * Highlight ROT47
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlight(pos, args) {
+        return pos;
+    }
+
+    /**
+     * Highlight ROT47 in reverse
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlightReverse(pos, args) {
+        return pos;
+    }
+}
+
+export default ROT47;

+ 81 - 0
src/core/operations/RotateLeft.mjs

@@ -0,0 +1,81 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import {rot, rotl, rotlCarry} from "../lib/Rotate";
+
+
+/**
+ * Rotate left operation.
+ */
+class RotateLeft extends Operation {
+
+    /**
+     * RotateLeft constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Rotate left";
+        this.module = "Default";
+        this.description = "Rotates each byte to the left by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.";
+        this.inputType = "byteArray";
+        this.outputType = "byteArray";
+        this.args = [
+            {
+                name: "Amount",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Carry through",
+                type: "boolean",
+                value: false
+            }
+        ];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        if (args[1]) {
+            return rotlCarry(input, args[0]);
+        } else {
+            return rot(input, args[0], rotl);
+        }
+    }
+
+    /**
+     * Highlight rotate left
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlight(pos, args) {
+        return pos;
+    }
+
+    /**
+     * Highlight rotate left in reverse
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlightReverse(pos, args) {
+        return pos;
+    }
+}
+
+export default RotateLeft;

+ 81 - 0
src/core/operations/RotateRight.mjs

@@ -0,0 +1,81 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import {rot, rotr, rotrCarry} from "../lib/Rotate";
+
+
+/**
+ * Rotate right operation.
+ */
+class RotateRight extends Operation {
+
+    /**
+     * RotateRight constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Rotate right";
+        this.module = "Default";
+        this.description = "Rotates each byte to the right by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.";
+        this.inputType = "byteArray";
+        this.outputType = "byteArray";
+        this.args = [
+            {
+                name: "Amount",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Carry through",
+                type: "boolean",
+                value: false
+            }
+        ];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        if (args[1]) {
+            return rotrCarry(input, args[0]);
+        } else {
+            return rot(input, args[0], rotr);
+        }
+    }
+
+    /**
+     * Highlight rotate right
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlight(pos, args) {
+        return pos;
+    }
+
+    /**
+     * Highlight rotate right in reverse
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlightReverse(pos, args) {
+        return pos;
+    }
+}
+
+export default RotateRight;

+ 8 - 0
src/core/operations/index.mjs

@@ -10,8 +10,12 @@ import FromBase64 from "./FromBase64";
 import FromHex from "./FromHex";
 import Gunzip from "./Gunzip";
 import Gzip from "./Gzip";
+import ROT13 from "./ROT13";
+import ROT47 from "./ROT47";
 import RawDeflate from "./RawDeflate";
 import RawInflate from "./RawInflate";
+import RotateLeft from "./RotateLeft";
+import RotateRight from "./RotateRight";
 import ShowBase64Offsets from "./ShowBase64Offsets";
 import ToBase32 from "./ToBase32";
 import ToBase64 from "./ToBase64";
@@ -27,8 +31,12 @@ export {
     FromHex,
     Gunzip,
     Gzip,
+    ROT13,
+    ROT47,
     RawDeflate,
     RawInflate,
+    RotateLeft,
+    RotateRight,
     ShowBase64Offsets,
     ToBase32,
     ToBase64,

+ 0 - 244
src/core/operations/legacy/Rotate.js

@@ -1,244 +0,0 @@
-/**
- * Bit rotation operations.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @namespace
- *
- * @todo Support for UTF16
- */
-const Rotate = {
-
-    /**
-     * @constant
-     * @default
-     */
-    ROTATE_AMOUNT: 1,
-    /**
-     * @constant
-     * @default
-     */
-    ROTATE_CARRY: false,
-
-    /**
-     * Runs rotation operations across the input data.
-     *
-     * @private
-     * @param {byteArray} data
-     * @param {number} amount
-     * @param {function} algo - The rotation operation to carry out
-     * @returns {byteArray}
-     */
-    _rot: function(data, amount, algo) {
-        const result = [];
-        for (let i = 0; i < data.length; i++) {
-            let b = data[i];
-            for (let j = 0; j < amount; j++) {
-                b = algo(b);
-            }
-            result.push(b);
-        }
-        return result;
-    },
-
-
-    /**
-     * Rotate right operation.
-     *
-     * @param {byteArray} input
-     * @param {Object[]} args
-     * @returns {byteArray}
-     */
-    runRotr: function(input, args) {
-        if (args[1]) {
-            return Rotate._rotrCarry(input, args[0]);
-        } else {
-            return Rotate._rot(input, args[0], Rotate._rotr);
-        }
-    },
-
-
-    /**
-     * Rotate left operation.
-     *
-     * @param {byteArray} input
-     * @param {Object[]} args
-     * @returns {byteArray}
-     */
-    runRotl: function(input, args) {
-        if (args[1]) {
-            return Rotate._rotlCarry(input, args[0]);
-        } else {
-            return Rotate._rot(input, args[0], Rotate._rotl);
-        }
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    ROT13_AMOUNT: 13,
-    /**
-     * @constant
-     * @default
-     */
-    ROT13_LOWERCASE: true,
-    /**
-     * @constant
-     * @default
-     */
-    ROT13_UPPERCASE: true,
-
-    /**
-     * ROT13 operation.
-     *
-     * @param {byteArray} input
-     * @param {Object[]} args
-     * @returns {byteArray}
-     */
-    runRot13: function(input, args) {
-        let amount = args[2],
-            output = input,
-            chr,
-            rot13Lowercase = args[0],
-            rot13Upperacse = args[1];
-
-        if (amount) {
-            if (amount < 0) {
-                amount = 26 - (Math.abs(amount) % 26);
-            }
-
-            for (let i = 0; i < input.length; i++) {
-                chr = input[i];
-                if (rot13Upperacse && chr >= 65 && chr <= 90) { // Upper case
-                    chr = (chr - 65 + amount) % 26;
-                    output[i] = chr + 65;
-                } else if (rot13Lowercase && chr >= 97 && chr <= 122) { // Lower case
-                    chr = (chr - 97 + amount) % 26;
-                    output[i] = chr + 97;
-                }
-            }
-        }
-        return output;
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    ROT47_AMOUNT: 47,
-
-    /**
-     * ROT47 operation.
-     *
-     * @author Matt C [matt@artemisbot.uk]
-     * @param {byteArray} input
-     * @param {Object[]} args
-     * @returns {byteArray}
-     */
-    runRot47: function(input, args) {
-        let amount = args[0],
-            output = input,
-            chr;
-
-        if (amount) {
-            if (amount < 0) {
-                amount = 94 - (Math.abs(amount) % 94);
-            }
-
-            for (let i = 0; i < input.length; i++) {
-                chr = input[i];
-                if (chr >= 33 && chr <= 126) {
-                    chr = (chr - 33 + amount) % 94;
-                    output[i] = chr + 33;
-                }
-            }
-        }
-        return output;
-    },
-
-
-    /**
-     * Rotate right bitwise op.
-     *
-     * @private
-     * @param {byte} b
-     * @returns {byte}
-     */
-    _rotr: function(b) {
-        const bit = (b & 1) << 7;
-        return (b >> 1) | bit;
-    },
-
-
-    /**
-     * Rotate left bitwise op.
-     *
-     * @private
-     * @param {byte} b
-     * @returns {byte}
-     */
-    _rotl: function(b) {
-        const bit = (b >> 7) & 1;
-        return ((b << 1) | bit) & 0xFF;
-    },
-
-
-    /**
-     * Rotates a byte array to the right by a specific amount as a whole, so that bits are wrapped
-     * from the end of the array to the beginning.
-     *
-     * @private
-     * @param {byteArray} data
-     * @param {number} amount
-     * @returns {byteArray}
-     */
-    _rotrCarry: function(data, amount) {
-        let carryBits = 0,
-            newByte,
-            result = [];
-
-        amount = amount % 8;
-        for (let i = 0; i < data.length; i++) {
-            const oldByte = data[i] >>> 0;
-            newByte = (oldByte >> amount) | carryBits;
-            carryBits = (oldByte & (Math.pow(2, amount)-1)) << (8-amount);
-            result.push(newByte);
-        }
-        result[0] |= carryBits;
-        return result;
-    },
-
-
-    /**
-     * Rotates a byte array to the left by a specific amount as a whole, so that bits are wrapped
-     * from the beginning of the array to the end.
-     *
-     * @private
-     * @param {byteArray} data
-     * @param {number} amount
-     * @returns {byteArray}
-     */
-    _rotlCarry: function(data, amount) {
-        let carryBits = 0,
-            newByte,
-            result = [];
-
-        amount = amount % 8;
-        for (let i = data.length-1; i >= 0; i--) {
-            const oldByte = data[i];
-            newByte = ((oldByte << amount) | carryBits) & 0xFF;
-            carryBits = (oldByte >> (8-amount)) & (Math.pow(2, amount)-1);
-            result[i] = (newByte);
-        }
-        result[data.length-1] = result[data.length-1] | carryBits;
-        return result;
-    },
-
-};
-
-export default Rotate;

+ 1 - 0
test/index.mjs

@@ -45,6 +45,7 @@ import "./tests/operations/Base64";
 // import "./tests/operations/NetBIOS.js";
 // import "./tests/operations/OTP.js";
 // import "./tests/operations/Regex.js";
+import "./tests/operations/Rotate.mjs";
 // import "./tests/operations/StrUtils.js";
 // import "./tests/operations/SeqUtils.js";
 

+ 215 - 0
test/tests/operations/Rotate.mjs

@@ -0,0 +1,215 @@
+/**
+ * Rotate tests.
+ *
+ * @author Matt C [matt@artemisbot.uk]
+ *
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister";
+
+
+TestRegister.addTests([
+    {
+        name: "Rotate left: nothing",
+        input: "",
+        expectedOutput: "",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate left",
+                args: [1, false],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "Rotate left: normal",
+        input: "61 62 63 31 32 33",
+        expectedOutput: "c2 c4 c6 62 64 66",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate left",
+                args: [1, false],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "Rotate left: carry",
+        input: "61 62 63 31 32 33",
+        expectedOutput: "85 89 8c c4 c8 cd",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate left",
+                args: [2, true],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "Rotate right: nothing",
+        input: "",
+        expectedOutput: "",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate right",
+                args: [1, false],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "Rotate right: normal",
+        input: "61 62 63 31 32 33",
+        expectedOutput: "b0 31 b1 98 19 99",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate right",
+                args: [1, false],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "Rotate right: carry",
+        input: "61 62 63 31 32 33",
+        expectedOutput: "d8 58 98 cc 4c 8c",
+        recipeConfig: [
+            {
+                op: "From Hex",
+                args: ["Space"]
+            },
+            {
+                op: "Rotate right",
+                args: [2, true],
+            },
+            {
+                op: "To Hex",
+                args: ["Space"]
+            }
+        ],
+    },
+    {
+        name: "ROT13: nothing",
+        input: "",
+        expectedOutput: "",
+        recipeConfig: [
+            {
+                op: "ROT13",
+                args: [true, true, 13]
+            },
+        ],
+    },
+    {
+        name: "ROT13: normal",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "Gur Dhvpx Oebja Sbk Whzcrq Bire Gur Ynml Qbt.",
+        recipeConfig: [
+            {
+                op: "ROT13",
+                args: [true, true, 13]
+            },
+        ],
+    },
+    {
+        name: "ROT13: full loop",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        recipeConfig: [
+            {
+                op: "ROT13",
+                args: [true, true, 26]
+            },
+        ],
+    },
+    {
+        name: "ROT13: lowercase only",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "Tur Qhvpx Bebja Fbk Jhzcrq Oire Tur Lnml Dbt.",
+        recipeConfig: [
+            {
+                op: "ROT13",
+                args: [true, false, 13]
+            },
+        ],
+    },
+    {
+        name: "ROT13: uppercase only",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "Ghe Duick Orown Sox Wumped Bver Ghe Yazy Qog.",
+        recipeConfig: [
+            {
+                op: "ROT13",
+                args: [false, true, 13]
+            },
+        ],
+    },
+    {
+        name: "ROT47: nothing",
+        input: "",
+        expectedOutput: "",
+        recipeConfig: [
+            {
+                op: "ROT47",
+                args: [47]
+            },
+        ],
+    },
+    {
+        name: "ROT47: normal",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "%96 \"F:4< qC@H? u@I yF>A65 ~G6C %96 {2KJ s@8]",
+        recipeConfig: [
+            {
+                op: "ROT47",
+                args: [47]
+            },
+        ],
+    },
+    {
+        name: "ROT47: full loop",
+        input: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        expectedOutput: "The Quick Brown Fox Jumped Over The Lazy Dog.",
+        recipeConfig: [
+            {
+                op: "ROT47",
+                args: [94]
+            },
+        ],
+    },
+]);