From 4988ead918eefbd22616505991362c55c915e618 Mon Sep 17 00:00:00 2001 From: Matt C Date: Tue, 3 Apr 2018 22:50:26 +0100 Subject: [PATCH 1/5] Rotate module converted to ESM 4 Ops: - ROT-13 - ROT-47 - Rotate left - Rotate right + module containing common functions --- src/core/config/Categories.js | 20 ++--- src/core/config/OperationConfig.json | 78 ++++++++++++++++++- src/core/config/modules/Default.mjs | 8 ++ src/core/lib/Rotate.mjs | 112 +++++++++++++++++++++++++++ src/core/operations/ROT13.mjs | 110 ++++++++++++++++++++++++++ src/core/operations/ROT47.mjs | 93 ++++++++++++++++++++++ src/core/operations/RotateLeft.mjs | 80 +++++++++++++++++++ src/core/operations/RotateRight.mjs | 80 +++++++++++++++++++ src/core/operations/index.mjs | 8 ++ 9 files changed, 578 insertions(+), 11 deletions(-) create mode 100644 src/core/lib/Rotate.mjs create mode 100644 src/core/operations/ROT13.mjs create mode 100644 src/core/operations/ROT47.mjs create mode 100644 src/core/operations/RotateLeft.mjs create mode 100644 src/core/operations/RotateRight.mjs diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 77eafb4b..b74e932a 100755 --- a/src/core/config/Categories.js +++ b/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: [ diff --git a/src/core/config/OperationConfig.json b/src/core/config/OperationConfig.json index 1486c66d..2f18363d 100644 --- a/src/core/config/OperationConfig.json +++ b/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.

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": [ { diff --git a/src/core/config/modules/Default.mjs b/src/core/config/modules/Default.mjs index 34043043..8648aa8a 100644 --- a/src/core/config/modules/Default.mjs +++ b/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, diff --git a/src/core/lib/Rotate.mjs b/src/core/lib/Rotate.mjs new file mode 100644 index 00000000..97c4b1df --- /dev/null +++ b/src/core/lib/Rotate.mjs @@ -0,0 +1,112 @@ +/** + * Bit rotation functions. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + * + * @todo Support for UTF16 + */ + + +/** + * Default values for rotation operations + */ +export const ROTATE_AMOUNT = 1; +export const ROTATE_CARRY = false; + + +/** + * 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. + * + * @private + * @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. + * + * @private + * @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; +} diff --git a/src/core/operations/ROT13.mjs b/src/core/operations/ROT13.mjs new file mode 100644 index 00000000..3bac332a --- /dev/null +++ b/src/core/operations/ROT13.mjs @@ -0,0 +1,110 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; + +/** + * Default arguments for ROT13 operation + */ +const ROT13_AMOUNT = 13, + ROT13_LOWERCASE = true, + ROT13_UPPERCASE = true; + + +/** + * 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: ROT13_LOWERCASE + }, + { + name: "Rotate upper case chars", + type: "boolean", + value: ROT13_UPPERCASE + }, + { + name: "Amount", + type: "number", + value: ROT13_AMOUNT + }, + ]; + } + + /** + * @param {string} 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; diff --git a/src/core/operations/ROT47.mjs b/src/core/operations/ROT47.mjs new file mode 100644 index 00000000..0421438b --- /dev/null +++ b/src/core/operations/ROT47.mjs @@ -0,0 +1,93 @@ +/** + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; + +/** + * Default argument for ROT47 operation + */ +const ROT47_AMOUNT = 47; + + +/** + * 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: ROT47_AMOUNT + }, + ]; + } + + /** + * @param {string} 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; diff --git a/src/core/operations/RotateLeft.mjs b/src/core/operations/RotateLeft.mjs new file mode 100644 index 00000000..84a14565 --- /dev/null +++ b/src/core/operations/RotateLeft.mjs @@ -0,0 +1,80 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import { rot, rotl, rotlCarry, ROTATE_AMOUNT, ROTATE_CARRY } 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: ROTATE_AMOUNT + }, + { + name: "Carry through", + type: "boolean", + value: ROTATE_CARRY + } + ]; + } + + /** + * @param {string} 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; diff --git a/src/core/operations/RotateRight.mjs b/src/core/operations/RotateRight.mjs new file mode 100644 index 00000000..2018c6fc --- /dev/null +++ b/src/core/operations/RotateRight.mjs @@ -0,0 +1,80 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import { rot, rotr, rotrCarry, ROTATE_AMOUNT, ROTATE_CARRY } 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: ROTATE_AMOUNT + }, + { + name: "Carry through", + type: "boolean", + value: ROTATE_CARRY + } + ]; + } + + /** + * @param {string} 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; diff --git a/src/core/operations/index.mjs b/src/core/operations/index.mjs index 55a8e79a..5b735963 100644 --- a/src/core/operations/index.mjs +++ b/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, From 7e86f02e4ed0921dca3f31ccff2fd57325a95526 Mon Sep 17 00:00:00 2001 From: Matt C Date: Tue, 3 Apr 2018 22:51:50 +0100 Subject: [PATCH 2/5] Deleted legacy Rotate module --- src/core/operations/legacy/Rotate.js | 244 --------------------------- 1 file changed, 244 deletions(-) delete mode 100755 src/core/operations/legacy/Rotate.js diff --git a/src/core/operations/legacy/Rotate.js b/src/core/operations/legacy/Rotate.js deleted file mode 100755 index 9125e575..00000000 --- a/src/core/operations/legacy/Rotate.js +++ /dev/null @@ -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; From af51090ebba6c6481d79d3bda78b29b6fe1f8420 Mon Sep 17 00:00:00 2001 From: Matt C Date: Tue, 3 Apr 2018 23:29:47 +0100 Subject: [PATCH 3/5] Added rotate ESM tests --- test/index.mjs | 1 + test/tests/operations/Rotate.mjs | 214 +++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 test/tests/operations/Rotate.mjs diff --git a/test/index.mjs b/test/index.mjs index bd3f27ba..82b3c081 100644 --- a/test/index.mjs +++ b/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"; diff --git a/test/tests/operations/Rotate.mjs b/test/tests/operations/Rotate.mjs new file mode 100644 index 00000000..e48373d4 --- /dev/null +++ b/test/tests/operations/Rotate.mjs @@ -0,0 +1,214 @@ +/** + * Base64 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] + }, + ], + }, +]); From fad4713a90d52b541ac6b2e5e2e25aabcc16083f Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 6 Apr 2018 12:40:39 +0000 Subject: [PATCH 4/5] ESM: Tidied up Rotate operations --- src/core/lib/Rotate.mjs | 9 --------- src/core/operations/ROT13.mjs | 15 ++++----------- src/core/operations/ROT47.mjs | 9 ++------- src/core/operations/RotateLeft.mjs | 9 +++++---- src/core/operations/RotateRight.mjs | 9 +++++---- test/tests/operations/Rotate.mjs | 3 ++- 6 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/core/lib/Rotate.mjs b/src/core/lib/Rotate.mjs index 97c4b1df..8d70fb25 100644 --- a/src/core/lib/Rotate.mjs +++ b/src/core/lib/Rotate.mjs @@ -9,13 +9,6 @@ */ -/** - * Default values for rotation operations - */ -export const ROTATE_AMOUNT = 1; -export const ROTATE_CARRY = false; - - /** * Runs rotation operations across the input data. * @@ -64,7 +57,6 @@ export function rotl(b) { * 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} @@ -90,7 +82,6 @@ export function rotrCarry(data, amount) { * 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} diff --git a/src/core/operations/ROT13.mjs b/src/core/operations/ROT13.mjs index 3bac332a..d3e4e75a 100644 --- a/src/core/operations/ROT13.mjs +++ b/src/core/operations/ROT13.mjs @@ -6,13 +6,6 @@ import Operation from "../Operation"; -/** - * Default arguments for ROT13 operation - */ -const ROT13_AMOUNT = 13, - ROT13_LOWERCASE = true, - ROT13_UPPERCASE = true; - /** * ROT13 operation. @@ -34,23 +27,23 @@ class ROT13 extends Operation { { name: "Rotate lower case chars", type: "boolean", - value: ROT13_LOWERCASE + value: true }, { name: "Rotate upper case chars", type: "boolean", - value: ROT13_UPPERCASE + value: true }, { name: "Amount", type: "number", - value: ROT13_AMOUNT + value: 13 }, ]; } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {byteArray} */ diff --git a/src/core/operations/ROT47.mjs b/src/core/operations/ROT47.mjs index 0421438b..e0722670 100644 --- a/src/core/operations/ROT47.mjs +++ b/src/core/operations/ROT47.mjs @@ -6,11 +6,6 @@ import Operation from "../Operation"; -/** - * Default argument for ROT47 operation - */ -const ROT47_AMOUNT = 47; - /** * ROT47 operation. @@ -32,13 +27,13 @@ class ROT47 extends Operation { { name: "Amount", type: "number", - value: ROT47_AMOUNT + value: 47 }, ]; } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {byteArray} */ diff --git a/src/core/operations/RotateLeft.mjs b/src/core/operations/RotateLeft.mjs index 84a14565..d21e0def 100644 --- a/src/core/operations/RotateLeft.mjs +++ b/src/core/operations/RotateLeft.mjs @@ -5,7 +5,8 @@ */ import Operation from "../Operation"; -import { rot, rotl, rotlCarry, ROTATE_AMOUNT, ROTATE_CARRY } from "../lib/Rotate"; +import {rot, rotl, rotlCarry} from "../lib/Rotate"; + /** * Rotate left operation. @@ -27,18 +28,18 @@ class RotateLeft extends Operation { { name: "Amount", type: "number", - value: ROTATE_AMOUNT + value: 1 }, { name: "Carry through", type: "boolean", - value: ROTATE_CARRY + value: false } ]; } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {byteArray} */ diff --git a/src/core/operations/RotateRight.mjs b/src/core/operations/RotateRight.mjs index 2018c6fc..f4a35cb6 100644 --- a/src/core/operations/RotateRight.mjs +++ b/src/core/operations/RotateRight.mjs @@ -5,7 +5,8 @@ */ import Operation from "../Operation"; -import { rot, rotr, rotrCarry, ROTATE_AMOUNT, ROTATE_CARRY } from "../lib/Rotate"; +import {rot, rotr, rotrCarry} from "../lib/Rotate"; + /** * Rotate right operation. @@ -27,18 +28,18 @@ class RotateRight extends Operation { { name: "Amount", type: "number", - value: ROTATE_AMOUNT + value: 1 }, { name: "Carry through", type: "boolean", - value: ROTATE_CARRY + value: false } ]; } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {byteArray} */ diff --git a/test/tests/operations/Rotate.mjs b/test/tests/operations/Rotate.mjs index e48373d4..d7ba9af1 100644 --- a/test/tests/operations/Rotate.mjs +++ b/test/tests/operations/Rotate.mjs @@ -1,5 +1,5 @@ /** - * Base64 tests. + * Rotate tests. * * @author Matt C [matt@artemisbot.uk] * @@ -8,6 +8,7 @@ */ import TestRegister from "../../TestRegister"; + TestRegister.addTests([ { name: "Rotate left: nothing", From b7ed1becba57e2e9527b86dbcfb86efdd8c1129e Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 6 Apr 2018 18:11:13 +0000 Subject: [PATCH 5/5] ESM: Added new List Dish type. Added present() method for displaying operation output in a nice way. Testing required. --- src/core/Chef.mjs | 4 +- src/core/Dish.mjs | 37 +++++++-- src/core/FlowControl.js | 12 +-- src/core/Operation.mjs | 38 +++++++++ src/core/Recipe.mjs | 10 ++- src/core/Utils.mjs | 96 +++++++++++++++------- src/core/config/scripts/generateConfig.mjs | 2 +- src/core/operations/Unzip.mjs | 35 ++++---- 8 files changed, 165 insertions(+), 69 deletions(-) diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index 9c52695e..a935d75c 100755 --- a/src/core/Chef.mjs +++ b/src/core/Chef.mjs @@ -91,8 +91,8 @@ class Chef { return { result: this.dish.type === Dish.HTML ? - this.dish.get(Dish.HTML, notUTF8) : - this.dish.get(returnType, notUTF8), + await this.dish.get(Dish.HTML, notUTF8) : + await this.dish.get(returnType, notUTF8), type: Dish.enumLookup(this.dish.type), progress: progress, duration: new Date().getTime() - startTime, diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs index 792395c1..6aeaf3e9 100755 --- a/src/core/Dish.mjs +++ b/src/core/Dish.mjs @@ -51,6 +51,8 @@ class Dish { case "bignumber": case "big number": return Dish.BIG_NUMBER; + case "list": + return Dish.LIST_FILE; default: throw "Invalid data type string. No matching enum."; } @@ -77,6 +79,8 @@ class Dish { return "ArrayBuffer"; case Dish.BIG_NUMBER: return "BigNumber"; + case Dish.LIST_FILE: + return "List"; default: throw "Invalid data type enum. No matching type."; } @@ -86,7 +90,7 @@ class Dish { /** * Sets the data value and type and then validates them. * - * @param {byteArray|string|number|ArrayBuffer|BigNumber} value + * @param {*} value * - The value of the input data. * @param {number} type * - The data type of value, see Dish enums. @@ -112,15 +116,14 @@ class Dish { * * @param {number} type - The data type of value, see Dish enums. * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. - * @returns {byteArray|string|number|ArrayBuffer|BigNumber} - * The value of the output data. + * @returns {*} - The value of the output data. */ - get(type, notUTF8=false) { + async get(type, notUTF8=false) { if (typeof type === "string") { type = Dish.typeEnum(type); } if (this.type !== type) { - this.translate(type, notUTF8); + await this._translate(type, notUTF8); } return this.value; } @@ -132,7 +135,7 @@ class Dish { * @param {number} toType - The data type of value, see Dish enums. * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. */ - translate(toType, notUTF8=false) { + async _translate(toType, notUTF8=false) { log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; @@ -142,7 +145,7 @@ class Dish { this.value = this.value ? Utils.strToByteArray(this.value) : []; break; case Dish.NUMBER: - this.value = typeof this.value == "number" ? Utils.strToByteArray(this.value.toString()) : []; + this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : []; break; case Dish.HTML: this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; @@ -154,6 +157,11 @@ class Dish { case Dish.BIG_NUMBER: this.value = this.value instanceof BigNumber ? Utils.strToByteArray(this.value.toFixed()) : []; break; + case Dish.LIST_FILE: + this.value = await Promise.all(this.value.map(async f => Utils.readFile(f))); + this.value = this.value.map(b => Array.prototype.slice.call(b)); + this.value = [].concat.apply([], this.value); + break; default: break; } @@ -183,6 +191,10 @@ class Dish { } this.type = Dish.BIG_NUMBER; break; + case Dish.LIST_FILE: + this.value = new File(this.value, "unknown"); + this.type = Dish.LIST_FILE; + break; default: break; } @@ -220,6 +232,9 @@ class Dish { return this.value instanceof ArrayBuffer; case Dish.BIG_NUMBER: return this.value instanceof BigNumber; + case Dish.LIST_FILE: + return this.value instanceof Array && + this.value.reduce((acc, curr) => acc && curr instanceof File, true); default: return false; } @@ -244,6 +259,8 @@ class Dish { return this.value.toString().length; case Dish.ARRAY_BUFFER: return this.value.byteLength; + case Dish.LIST_FILE: + return this.value.reduce((acc, curr) => acc + curr.size, 0); default: return -1; } @@ -288,6 +305,12 @@ Dish.ARRAY_BUFFER = 4; * @enum */ Dish.BIG_NUMBER = 5; +/** + * Dish data type enum for lists of files. + * @readonly + * @enum + */ +Dish.LIST_FILE = 6; export default Dish; diff --git a/src/core/FlowControl.js b/src/core/FlowControl.js index 73f29d78..92440c49 100755 --- a/src/core/FlowControl.js +++ b/src/core/FlowControl.js @@ -26,7 +26,7 @@ const FlowControl = { const opList = state.opList, inputType = opList[state.progress].inputType, outputType = opList[state.progress].outputType, - input = state.dish.get(inputType), + input = await state.dish.get(inputType), ings = opList[state.progress].ingValues, splitDelim = ings[0], mergeDelim = ings[1], @@ -77,7 +77,7 @@ const FlowControl = { } progress = err.progress + 1; } - output += dish.get(outputType) + mergeDelim; + output += await dish.get(outputType) + mergeDelim; } state.dish.set(output, outputType); @@ -111,7 +111,7 @@ const FlowControl = { * @param {Operation[]} state.opList - The list of operations in the recipe. * @returns {Object} The updated state of the recipe. */ - runRegister: function(state) { + runRegister: async function(state) { const ings = state.opList[state.progress].ingValues, extractorStr = ings[0], i = ings[1], @@ -122,7 +122,7 @@ const FlowControl = { if (m) modifiers += "m"; const extractor = new RegExp(extractorStr, modifiers), - input = state.dish.get(Dish.STRING), + input = await state.dish.get(Dish.STRING), registers = input.match(extractor); if (!registers) return state; @@ -208,7 +208,7 @@ const FlowControl = { * @param {number} state.numJumps - The number of jumps taken so far. * @returns {Object} The updated state of the recipe. */ - runCondJump: function(state) { + runCondJump: async function(state) { const ings = state.opList[state.progress].ingValues, dish = state.dish, regexStr = ings[0], @@ -223,7 +223,7 @@ const FlowControl = { } if (regexStr !== "") { - const strMatch = dish.get(Dish.STRING).search(regexStr) > -1; + const strMatch = await dish.get(Dish.STRING).search(regexStr) > -1; if (!invert && strMatch || invert && !strMatch) { state.progress = jmpIndex; state.numJumps++; diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs index 30ad71e4..e87c31f4 100755 --- a/src/core/Operation.mjs +++ b/src/core/Operation.mjs @@ -19,6 +19,7 @@ class Operation { // Private fields this._inputType = -1; this._outputType = -1; + this._presentType = -1; this._breakpoint = false; this._disabled = false; this._flowControl = false; @@ -71,6 +72,22 @@ class Operation { } + /** + * Method to be called when displaying the result of an operation in a human-readable + * format. This allows operations to return usable data from their run() method and + * only format them when this method is called. + * + * The default action is to return the data unchanged, but child classes can override + * this behaviour. + * + * @param {*} data - The result of the run() function + * @returns {*} - A human-readable version of the data + */ + present(data) { + return data; + } + + /** * Sets the input type as a Dish enum. * @@ -98,6 +115,7 @@ class Operation { */ set outputType(typeStr) { this._outputType = Dish.typeEnum(typeStr); + if (this._presentType < 0) this._presentType = this._outputType; } @@ -111,6 +129,26 @@ class Operation { } + /** + * Sets the presentation type as a Dish enum. + * + * @param {string} typeStr + */ + set presentType(typeStr) { + this._presentType = Dish.typeEnum(typeStr); + } + + + /** + * Gets the presentation type as a readable string. + * + * @returns {string} + */ + get presentType() { + return Dish.enumLookup(this._presentType); + } + + /** * Sets the args for the current operation. * diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index 7afaeab9..14792157 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -130,7 +130,7 @@ class Recipe { * - The final progress through the recipe */ async execute(dish, startFrom=0, forkState={}) { - let op, input, output, + let op, input, output, lastRunOp, numJumps = 0, numRegisters = forkState.numRegisters || 0; @@ -149,7 +149,7 @@ class Recipe { } try { - input = dish.get(op.inputType); + input = await dish.get(op.inputType); log.debug("Executing operation"); if (op.flowControl) { @@ -169,6 +169,7 @@ class Recipe { numRegisters = state.numRegisters; } else { output = await op.run(input, op.ingValues); + lastRunOp = op; dish.set(output, op.outputType); } } catch (err) { @@ -187,6 +188,11 @@ class Recipe { } } + // Present the results of the final operation + // TODO try/catch + output = await lastRunOp.present(output); + dish.set(output, lastRunOp.presentType); + log.debug("Recipe complete"); return this.opList.length; } diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 2be207eb..88cfa52e 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -812,35 +812,30 @@ class Utils { /** * Formats a list of files or directories. - * A File is an object with a "fileName" and optionally a "contents". - * If the fileName ends with "/" and the contents is of length 0 then - * it is considered a directory. * * @author tlwr [toby@toby.codes] + * @author n1474335 [n1474335@gmail.com] * - * @param {Object[]} files + * @param {File[]} files * @returns {html} */ - static displayFilesAsHTML(files) { - /* and used to denote newlines and spaces in HTML markup. - * If a non-html operation is used, all markup will be removed but these - * whitespace chars will remain for formatting purposes. - */ - + static async displayFilesAsHTML(files) { const formatDirectory = function(file) { const html = `
`; return html; }; - const formatFile = function(file, i) { + const formatFile = async function(file, i) { + const buff = await Utils.readFile(file); + const fileStr = Utils.arrayBufferToStr(buff.buffer); const blob = new Blob( - [new Uint8Array(file.bytes)], + [buff], {type: "octet/stream"} ); const blobUrl = URL.createObjectURL(blob); @@ -850,13 +845,13 @@ class Utils { data-toggle='collapse' aria-expanded='true' aria-controls='collapse${i}' - title="Show/hide contents of '${Utils.escapeHtml(file.fileName)}'">👁️`; + title="Show/hide contents of '${Utils.escapeHtml(file.name)}'">👁️`; const downloadFileElem = `💾`; + title='Download ${Utils.escapeHtml(file.name)}' + download='${Utils.escapeHtml(file.name)}'>💾`; - const hexFileData = toHexFast(new Uint8Array(file.bytes)); + const hexFileData = toHexFast(buff); const switchToInputElem = `

- ${Utils.escapeHtml(file.fileName)} - ${viewFileElem} - ${downloadFileElem} - ${switchToInputElem} + ${Utils.escapeHtml(file.name)} + ${viewFileElem} + ${downloadFileElem} + ${switchToInputElem} - ${file.size.toLocaleString()} bytes + ${file.size.toLocaleString()} bytes

@@ -880,7 +875,7 @@ class Utils {
-
${Utils.escapeHtml(file.contents)}
+
${Utils.escapeHtml(fileStr)}
`; @@ -891,17 +886,15 @@ class Utils { ${files.length} file(s) found `; - files.forEach(function(file, i) { - if (typeof file.contents !== "undefined") { - html += formatFile(file, i); + for (let i = 0; i < files.length; i++) { + if (files[i].name.endsWith("/")) { + html += formatDirectory(files[i]); } else { - html += formatDirectory(file); + html += await formatFile(files[i], i); } - }); + } - return html.replace(/(?:(
(?:\n|.)*<\/pre>)|\s{2,})/g, "$1") // Remove whitespace from markup
-            .replace(//g, "\n") // Replace  with newlines
-            .replace(//g, " "); // Replace  with spaces
+        return html;
     }
 
 
@@ -941,6 +934,47 @@ class Utils {
     }
 
 
+    /**
+     * Reads a File and returns the data as a Uint8Array.
+     *
+     * @param {File} file
+     * @returns {Uint8Array}
+     *
+     * @example
+     * // returns Uint8Array(5) [104, 101, 108, 108, 111]
+     * await Utils.readFile(new File(["hello"], "test"))
+     */
+    static readFile(file) {
+        return new Promise((resolve, reject) => {
+            const reader = new FileReader();
+            const data = new Uint8Array(file.size);
+            let offset = 0;
+            const CHUNK_SIZE = 10485760; // 10MiB
+
+            const seek = function() {
+                if (offset >= file.size) {
+                    resolve(data);
+                    return;
+                }
+                const slice = file.slice(offset, offset + CHUNK_SIZE);
+                reader.readAsArrayBuffer(slice);
+            };
+
+            reader.onload = function(e) {
+                data.set(new Uint8Array(reader.result), offset);
+                offset += CHUNK_SIZE;
+                seek();
+            };
+
+            reader.onerror = function(e) {
+                reject(reader.error.message);
+            };
+
+            seek();
+        });
+    }
+
+
     /**
      * Actual modulo function, since % is actually the remainder function in JS.
      *
diff --git a/src/core/config/scripts/generateConfig.mjs b/src/core/config/scripts/generateConfig.mjs
index b8905d50..51af293a 100644
--- a/src/core/config/scripts/generateConfig.mjs
+++ b/src/core/config/scripts/generateConfig.mjs
@@ -38,7 +38,7 @@ for (const opObj in Ops) {
         module: op.module,
         description: op.description,
         inputType: op.inputType,
-        outputType: op.outputType,
+        outputType: op.presentType,
         flowControl: op.flowControl,
         args: op.args
     };
diff --git a/src/core/operations/Unzip.mjs b/src/core/operations/Unzip.mjs
index 1444551a..07f2a65e 100644
--- a/src/core/operations/Unzip.mjs
+++ b/src/core/operations/Unzip.mjs
@@ -25,7 +25,8 @@ class Unzip extends Operation {
         this.module = "Compression";
         this.description = "Decompresses data using the PKZIP algorithm and displays it per file, with support for passwords.";
         this.inputType = "byteArray";
-        this.outputType = "html";
+        this.outputType = "List";
+        this.presentType = "html";
         this.args = [
             {
                 name: "Password",
@@ -43,7 +44,7 @@ class Unzip extends Operation {
     /**
      * @param {byteArray} input
      * @param {Object[]} args
-     * @returns {html}
+     * @returns {File[]}
      */
     run(input, args) {
         const options = {
@@ -51,28 +52,22 @@ class Unzip extends Operation {
                 verify: args[1]
             },
             unzip = new Zlib.Unzip(input, options),
-            filenames = unzip.getFilenames(),
-            files = [];
+            filenames = unzip.getFilenames();
 
-        filenames.forEach(function(fileName) {
+        return filenames.map(fileName => {
             const bytes = unzip.decompress(fileName);
-            const contents = Utils.byteArrayToUtf8(bytes);
-
-            const file = {
-                fileName: fileName,
-                size: contents.length,
-            };
-
-            const isDir = contents.length === 0 && fileName.endsWith("/");
-            if (!isDir) {
-                file.bytes = bytes;
-                file.contents = contents;
-            }
-
-            files.push(file);
+            return new File([bytes], fileName);
         });
+    }
 
-        return Utils.displayFilesAsHTML(files);
+    /**
+     * Displays the files in HTML for web apps.
+     *
+     * @param {File[]} files
+     * @returns {html}
+     */
+    async present(files) {
+        return await Utils.displayFilesAsHTML(files);
     }
 
 }