diff --git a/package-lock.json b/package-lock.json index dc5f8fc8..a53169bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "dependencies": { "@babel/polyfill": "^7.12.1", + "@blu3r4y/lzma": "^2.3.3", "arrive": "^2.4.1", "avsc": "^5.7.4", "bcryptjs": "^2.4.3", @@ -1771,6 +1772,14 @@ "node": ">=6.9.0" } }, + "node_modules/@blu3r4y/lzma": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@blu3r4y/lzma/-/lzma-2.3.3.tgz", + "integrity": "sha512-2ckRSsYewLAgq/s8tUW3o5gurtCNYga1f9l0egV4QlT8hgVEilQHRt18s+behmPL2M/BPBxUINaOz67u++r0wA==", + "bin": { + "lzma.js": "bin/lzma.js" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -15446,6 +15455,11 @@ "to-fast-properties": "^2.0.0" } }, + "@blu3r4y/lzma": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@blu3r4y/lzma/-/lzma-2.3.3.tgz", + "integrity": "sha512-2ckRSsYewLAgq/s8tUW3o5gurtCNYga1f9l0egV4QlT8hgVEilQHRt18s+behmPL2M/BPBxUINaOz67u++r0wA==" + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", diff --git a/package.json b/package.json index 11b39874..0eda9a6b 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ }, "dependencies": { "@babel/polyfill": "^7.12.1", + "@blu3r4y/lzma": "^2.3.3", "arrive": "^2.4.1", "avsc": "^5.7.4", "bcryptjs": "^2.4.3", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8ac60048..7869893a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -330,8 +330,10 @@ "Bzip2 Compress", "Tar", "Untar", + "LZString Decompress", "LZString Compress", - "LZString Decompress" + "LZMA Decompress", + "LZMA Compress" ] }, { diff --git a/src/core/operations/LZMACompress.mjs b/src/core/operations/LZMACompress.mjs new file mode 100644 index 00000000..5a252db2 --- /dev/null +++ b/src/core/operations/LZMACompress.mjs @@ -0,0 +1,64 @@ +/** + * @author Matt C [me@mitt.dev] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +import { compress } from "@blu3r4y/lzma"; +import {isWorkerEnvironment} from "../Utils.mjs"; + +/** + * LZMA Compress operation + */ +class LZMACompress extends Operation { + + /** + * LZMACompress constructor + */ + constructor() { + super(); + + this.name = "LZMA Compress"; + this.module = "Compression"; + this.description = "Compresses data using the Lempel\u2013Ziv\u2013Markov chain algorithm. Compression mode determines the speed and effectiveness of the compression: 1 is fastest and less effective, 9 is slowest and most effective"; + this.infoURL = "https://wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Markov_chain_algorithm"; + this.inputType = "ArrayBuffer"; + this.outputType = "ArrayBuffer"; + this.args = [ + { + name: "Compression Mode", + type: "option", + value: [ + "1", "2", "3", "4", "5", "6", "7", "8", "9" + ], + "defaultIndex": 6 + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {ArrayBuffer} + */ + async run(input, args) { + const mode = Number(args[0]); + return new Promise((resolve, reject) => { + compress(new Uint8Array(input), mode, (result, error) => { + if (error) { + reject(new OperationError(`Failed to compress input: ${error.message}`)); + } + // The compression returns as an Int8Array, but we can just get the unsigned data from the buffer + resolve(new Int8Array(result).buffer); + }, (percent) => { + if (isWorkerEnvironment()) self.sendStatusMessage(`Compressing input: ${(percent*100).toFixed(2)}%`); + }); + }); + } + +} + +export default LZMACompress; diff --git a/src/core/operations/LZMADecompress.mjs b/src/core/operations/LZMADecompress.mjs new file mode 100644 index 00000000..3bebb860 --- /dev/null +++ b/src/core/operations/LZMADecompress.mjs @@ -0,0 +1,57 @@ +/** + * @author Matt C [me@mitt.dev] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import {decompress} from "@blu3r4y/lzma"; +import Utils, {isWorkerEnvironment} from "../Utils.mjs"; + +/** + * LZMA Decompress operation + */ +class LZMADecompress extends Operation { + + /** + * LZMADecompress constructor + */ + constructor() { + super(); + + this.name = "LZMA Decompress"; + this.module = "Compression"; + this.description = "Decompresses data using the Lempel-Ziv-Markov chain Algorithm."; + this.infoURL = "https://wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Markov_chain_algorithm"; + this.inputType = "ArrayBuffer"; + this.outputType = "ArrayBuffer"; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {ArrayBuffer} + */ + async run(input, args) { + return new Promise((resolve, reject) => { + decompress(new Uint8Array(input), (result, error) => { + if (error) { + reject(new OperationError(`Failed to decompress input: ${error.message}`)); + } + // The decompression returns either a String or an untyped unsigned int8 array, but we can just get the unsigned data from the buffer + + if (typeof result == "string") { + resolve(Utils.strToArrayBuffer(result)); + } else { + resolve(new Int8Array(result).buffer); + } + }, (percent) => { + if (isWorkerEnvironment()) self.sendStatusMessage(`Decompressing input: ${(percent*100).toFixed(2)}%`); + }); + }); + } + +} + +export default LZMADecompress; diff --git a/tests/operations/tests/Compress.mjs b/tests/operations/tests/Compress.mjs index a1e895bb..015277b1 100644 --- a/tests/operations/tests/Compress.mjs +++ b/tests/operations/tests/Compress.mjs @@ -23,4 +23,56 @@ TestRegister.addTests([ } ], }, + { + name: "LZMA compress & decompress", + input: "The cat sat on the mat.", + // Generated using command `echo -n "The cat sat on the mat." | lzma -z -6 | xxd -p` + expectedOutput: "The cat sat on the mat.", + recipeConfig: [ + { + "op": "LZMA Compress", + "args": ["6"] + }, + { + "op": "LZMA Decompress", + "args": [] + }, + ], + }, + { + name: "LZMA decompress: binary", + // Generated using command `echo "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10" | xxd -r -p | lzma -z -6 | xxd -p` + input: "5d00008000ffffffffffffffff00000052500a84f99bb28021a969d627e03e8a922effffbd160000", + expectedOutput: "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Space"] + }, + { + "op": "LZMA Decompress", + "args": [] + }, + { + "op": "To Hex", + "args": ["Space", 0] + } + ], + }, + { + name: "LZMA decompress: string", + // Generated using command `echo -n "The cat sat on the mat." | lzma -z -6 | xxd -p` + input: "5d00008000ffffffffffffffff002a1a08a202b1a4b814b912c94c4152e1641907d3fd8cd903ffff4fec0000", + expectedOutput: "The cat sat on the mat.", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Space"] + }, + { + "op": "LZMA Decompress", + "args": [] + } + ], + }, ]);