From 1f09c03d4897206e7ed4a3d90cac6c577c486aed Mon Sep 17 00:00:00 2001 From: GCHQ 77703 Date: Fri, 15 Feb 2019 14:23:16 +0000 Subject: [PATCH 001/686] Add De Bruijn Operation --- src/core/config/Categories.json | 1 + .../operations/GenerateDeBruijnSequence.mjs | 87 +++++++++++++++++++ tests/operations/index.mjs | 1 + .../tests/GenerateDeBruijnSequence.mjs | 33 +++++++ 4 files changed, 122 insertions(+) create mode 100644 src/core/operations/GenerateDeBruijnSequence.mjs create mode 100644 tests/operations/tests/GenerateDeBruijnSequence.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8235ab10..238c7282 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -370,6 +370,7 @@ "Chi Square", "Disassemble x86", "Pseudo-Random Number Generator", + "Generate De Bruijn Sequence", "Generate UUID", "Generate TOTP", "Generate HOTP", diff --git a/src/core/operations/GenerateDeBruijnSequence.mjs b/src/core/operations/GenerateDeBruijnSequence.mjs new file mode 100644 index 00000000..647d3c7f --- /dev/null +++ b/src/core/operations/GenerateDeBruijnSequence.mjs @@ -0,0 +1,87 @@ +/** + * @author gchq77703 [gchq77703@gchq.gov.uk] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; + +/** + * Generate De Bruijn Sequence operation + */ +class GenerateDeBruijnSequence extends Operation { + + /** + * GenerateDeBruijnSequence constructor + */ + constructor() { + super(); + + this.name = "Generate De Bruijn Sequence"; + this.module = "Default"; + this.description = "Generates rolling keycode combinations given a certain alphabet size and key length."; + this.infoURL = "https://wikipedia.org/wiki/De_Bruijn_sequence"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Alphabet size (k)", + type: "number", + value: 2 + }, + { + name: "Key length (n)", + type: "number", + value: 3 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [k, n] = args; + + if (k < 2 || k > 9) { + throw new OperationError("Invalid alphabet size, required to be between 2 and 9 (inclusive)."); + } + + if (n < 2) { + throw new OperationError("Invalid key length, required to be at least 2."); + } + + if (Math.pow(k, n) > 50000) { + throw new OperationError("Too many permutations, please reduce k^n to under 50,000."); + } + + const a = []; + for (let i = 0; i < k * n; i++) a.push(0); + + const sequence = []; + + (function db(t = 1, p = 1) { + if (t > n) { + if (n % p !== 0) return; + for (let j = 1; j <= p; j++) { + sequence.push(a[j]); + } + return; + } + + a[t] = a[t - p]; + db(t + 1, p); + for (let j = a[t - p] + 1; j < k; j++) { + a[t] = j; + db(t + 1, t); + } + })(); + + return sequence.join(""); + } +} + +export default GenerateDeBruijnSequence; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index fb68ed9c..316e934c 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -45,6 +45,7 @@ import "./tests/DateTime"; import "./tests/ExtractEmailAddresses"; import "./tests/Fork"; import "./tests/FromDecimal"; +import "./tests/GenerateDeBruijnSequence"; import "./tests/Hash"; import "./tests/HaversineDistance"; import "./tests/Hexdump"; diff --git a/tests/operations/tests/GenerateDeBruijnSequence.mjs b/tests/operations/tests/GenerateDeBruijnSequence.mjs new file mode 100644 index 00000000..b68a843f --- /dev/null +++ b/tests/operations/tests/GenerateDeBruijnSequence.mjs @@ -0,0 +1,33 @@ +/** + * De Brujin Sequence tests. + * + * @author gchq77703 [gchq77703@gchq.gov.uk] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + name: "Small Sequence", + input: "", + expectedOutput: "00010111", + recipeConfig: [ + { + "op": "Generate De Bruijn Sequence", + "args": [2, 3] + } + ] + }, + { + name: "Long Sequence", + input: "", + expectedOutput: "0000010000200003000110001200013000210002200023000310003200033001010010200103001110011200113001210012200123001310013200133002010020200203002110021200213002210022200223002310023200233003010030200303003110031200313003210032200323003310033200333010110101201013010210102201023010310103201033011020110301111011120111301121011220112301131011320113301202012030121101212012130122101222012230123101232012330130201303013110131201313013210132201323013310133201333020210202202023020310203202033021030211102112021130212102122021230213102132021330220302211022120221302221022220222302231022320223302303023110231202313023210232202323023310233202333030310303203033031110311203113031210312203123031310313203133032110321203213032210322203223032310323203233033110331203313033210332203323033310333203333111112111131112211123111321113311212112131122211223112321123311312113131132211323113321133312122121231213212133122131222212223122321223312313123221232312332123331313213133132221322313232132331332213323133321333322222322233223232233323233233333", + recipeConfig: [ + { + "op": "Generate De Bruijn Sequence", + "args": [4, 5] + } + ] + } +]) \ No newline at end of file From 44a164ed2825ddd799b656459b89b1a4ee5a9f0a Mon Sep 17 00:00:00 2001 From: GCHQ 77703 Date: Tue, 19 Feb 2019 09:56:38 +0000 Subject: [PATCH 002/686] Fix test script linter --- tests/operations/tests/GenerateDeBruijnSequence.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/operations/tests/GenerateDeBruijnSequence.mjs b/tests/operations/tests/GenerateDeBruijnSequence.mjs index b68a843f..48e8c4ff 100644 --- a/tests/operations/tests/GenerateDeBruijnSequence.mjs +++ b/tests/operations/tests/GenerateDeBruijnSequence.mjs @@ -30,4 +30,4 @@ TestRegister.addTests([ } ] } -]) \ No newline at end of file +]); From 822a4fab86572817fcd2e6218d8c736d1e22bbf4 Mon Sep 17 00:00:00 2001 From: GCHQ 77703 Date: Tue, 19 Feb 2019 10:16:51 +0000 Subject: [PATCH 003/686] Fix operation linting --- src/core/operations/GenerateDeBruijnSequence.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/GenerateDeBruijnSequence.mjs b/src/core/operations/GenerateDeBruijnSequence.mjs index 647d3c7f..af788585 100644 --- a/src/core/operations/GenerateDeBruijnSequence.mjs +++ b/src/core/operations/GenerateDeBruijnSequence.mjs @@ -71,7 +71,7 @@ class GenerateDeBruijnSequence extends Operation { } return; } - + a[t] = a[t - p]; db(t + 1, p); for (let j = a[t - p] + 1; j < k; j++) { From 846e84d3a471513287f25d1e4071dbc5e970e272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karsten=20Silkenb=C3=A4umer?= Date: Sun, 3 Mar 2019 16:18:31 +0100 Subject: [PATCH 004/686] Add fernet encryption/decryption operation --- package-lock.json | 161 +++++++++++++++----------- package.json | 1 + src/core/config/Categories.json | 2 + src/core/operations/FernetDecrypt.mjs | 64 ++++++++++ src/core/operations/FernetEncrypt.mjs | 54 +++++++++ tests/operations/tests/Fernet.mjs | 80 +++++++++++++ 6 files changed, 292 insertions(+), 70 deletions(-) create mode 100644 src/core/operations/FernetDecrypt.mjs create mode 100644 src/core/operations/FernetEncrypt.mjs create mode 100644 tests/operations/tests/Fernet.mjs diff --git a/package-lock.json b/package-lock.json index 55ad6303..18da5b7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1631,7 +1631,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -1716,7 +1716,7 @@ }, "util": { "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -1864,7 +1864,7 @@ }, "axios": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "dev": true, "requires": { @@ -2334,7 +2334,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -2371,7 +2371,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -2436,7 +2436,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -2590,7 +2590,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -2639,7 +2639,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", @@ -3172,7 +3172,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -3185,7 +3185,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -3332,7 +3332,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { @@ -3700,7 +3700,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -3764,7 +3764,7 @@ "dependencies": { "domelementtype": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", "dev": true }, @@ -3969,7 +3969,7 @@ }, "entities": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", "dev": true }, @@ -4392,7 +4392,7 @@ }, "eventemitter2": { "version": "0.4.14", - "resolved": "http://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", "dev": true }, @@ -4404,7 +4404,7 @@ }, "events": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", "dev": true }, @@ -4720,6 +4720,22 @@ "pend": "~1.2.0" } }, + "fernet": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/fernet/-/fernet-0.3.1.tgz", + "integrity": "sha512-7KnsrcpLkUsKy6aH6Ow68hrMWhvE25rTDd3370+xVGkpqZta05cUCmdJQPyLBKTsNdPUB5NumJZBgJIJ60aQqw==", + "requires": { + "crypto-js": "~3.1.2-1", + "urlsafe-base64": "1.0.0" + }, + "dependencies": { + "crypto-js": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", + "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" + } + } + }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -4821,7 +4837,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -5057,7 +5073,7 @@ }, "fs-extra": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", "dev": true, "requires": { @@ -5726,7 +5742,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, @@ -5868,7 +5884,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -5945,7 +5961,7 @@ }, "grunt-cli": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", "dev": true, "requires": { @@ -5993,7 +6009,7 @@ "dependencies": { "shelljs": { "version": "0.5.3", - "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz", "integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM=", "dev": true } @@ -6013,7 +6029,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -6058,7 +6074,7 @@ }, "grunt-contrib-jshint": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz", "integrity": "sha1-Np2QmyWTxA6L55lAshNAhQx5Oaw=", "dev": true, "requires": { @@ -6157,7 +6173,7 @@ "dependencies": { "colors": { "version": "1.1.2", - "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true } @@ -6221,7 +6237,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -6482,7 +6498,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -6538,7 +6554,7 @@ }, "htmlparser2": { "version": "3.8.3", - "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "dev": true, "requires": { @@ -6557,7 +6573,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -6607,7 +6623,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -7053,7 +7069,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -7614,7 +7630,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, "requires": { @@ -7725,7 +7741,7 @@ }, "kew": { "version": "0.7.0", - "resolved": "http://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", "dev": true }, @@ -7844,7 +7860,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -7857,7 +7873,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -8221,7 +8237,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -8280,7 +8296,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -8501,7 +8517,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -8711,7 +8727,7 @@ }, "ncp": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=", "dev": true }, @@ -8810,7 +8826,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true } @@ -8993,7 +9009,7 @@ "dependencies": { "colors": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/colors/-/colors-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=" }, "underscore": { @@ -9287,13 +9303,13 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, "os-locale": { "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -9302,7 +9318,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -9338,7 +9354,7 @@ }, "p-is-promise": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", "dev": true }, @@ -9526,7 +9542,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { @@ -9612,7 +9628,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -9653,7 +9669,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -9836,7 +9852,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -10207,7 +10223,7 @@ }, "progress": { "version": "1.1.8", - "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" }, "promise-inflight": { @@ -10232,13 +10248,13 @@ "dependencies": { "async": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", "dev": true }, "winston": { "version": "2.1.1", - "resolved": "http://registry.npmjs.org/winston/-/winston-2.1.1.tgz", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", "dev": true, "requires": { @@ -10253,7 +10269,7 @@ "dependencies": { "colors": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", "dev": true }, @@ -10476,7 +10492,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -10665,7 +10681,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -10716,7 +10732,7 @@ }, "htmlparser2": { "version": "3.3.0", - "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", "dev": true, "requires": { @@ -10728,7 +10744,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -10995,7 +11011,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -11315,7 +11331,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -11359,7 +11375,7 @@ }, "shelljs": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", "dev": true }, @@ -12080,7 +12096,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -12097,7 +12113,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -12190,7 +12206,7 @@ }, "tar": { "version": "2.2.1", - "resolved": "http://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "dev": true, "requires": { @@ -12348,7 +12364,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -12942,6 +12958,11 @@ "requires-port": "^1.0.0" } }, + "urlsafe-base64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", + "integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY=" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -13008,7 +13029,7 @@ "dependencies": { "async": { "version": "0.9.2", - "resolved": "http://registry.npmjs.org/async/-/async-0.9.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", "dev": true }, @@ -13034,7 +13055,7 @@ }, "valid-data-url": { "version": "0.1.6", - "resolved": "http://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.6.tgz", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.6.tgz", "integrity": "sha512-FXg2qXMzfAhZc0y2HzELNfUeiOjPr+52hU1DNBWiJJ2luXD+dD1R9NA48Ug5aj0ibbxroeGDc/RJv6ThiGgkDw==", "dev": true }, @@ -13050,7 +13071,7 @@ }, "validator": { "version": "9.4.1", - "resolved": "http://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==", "dev": true }, @@ -13736,14 +13757,14 @@ "dependencies": { "async": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", "dev": true, "optional": true }, "colors": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", "dev": true, "optional": true @@ -13776,7 +13797,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/package.json b/package.json index cb59db38..35901453 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "esmangle": "^1.0.1", "esprima": "^4.0.1", "exif-parser": "^0.1.12", + "fernet": "^0.3.1", "file-saver": "^2.0.0", "geodesy": "^1.1.3", "highlight.js": "^9.13.1", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8235ab10..2db5af51 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -73,6 +73,8 @@ "DES Decrypt", "Triple DES Encrypt", "Triple DES Decrypt", + "Fernet Encrypt", + "Fernet Decrypt", "RC2 Encrypt", "RC2 Decrypt", "RC4", diff --git a/src/core/operations/FernetDecrypt.mjs b/src/core/operations/FernetDecrypt.mjs new file mode 100644 index 00000000..76d4fd16 --- /dev/null +++ b/src/core/operations/FernetDecrypt.mjs @@ -0,0 +1,64 @@ +/** + * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @copyright Karsten Silkenbäumer 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import fernet from "fernet"; + +/** + * FernetDecrypt operation + */ +class FernetDecrypt extends Operation { + /** + * FernetDecrypt constructor + */ + constructor() { + super(); + + this.name = "Fernet Decrypt"; + this.module = "Default"; + this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().

Key: The key must be 32 bytes (256 bits) encoded with Base64."; + this.infoURL = "https://asecuritysite.com/encryption/fer"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "string", + "value": "" + }, + ]; + this.patterns = [ + { + match: "^[A-Z\\d\\-_=]{20,}$", + flags: "i", + args: [] + }, + ]; + } + /** + * @param {String} input + * @param {Object[]} args + * @returns {String} + */ + run(input, args) { + const [secretInput] = args; + // const fernet = require("fernet"); + try { + const secret = new fernet.Secret(secretInput); + const token = new fernet.Token({ + secret: secret, + token: input, + ttl: 0 + }); + return token.decode(); + } catch (err) { + throw new OperationError(err); + } + } +} + +export default FernetDecrypt; diff --git a/src/core/operations/FernetEncrypt.mjs b/src/core/operations/FernetEncrypt.mjs new file mode 100644 index 00000000..ac8c64cb --- /dev/null +++ b/src/core/operations/FernetEncrypt.mjs @@ -0,0 +1,54 @@ +/** + * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @copyright Karsten Silkenbäumer 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; +import fernet from "fernet"; + +/** + * FernetEncrypt operation + */ +class FernetEncrypt extends Operation { + /** + * FernetEncrypt constructor + */ + constructor() { + super(); + + this.name = "Fernet Encrypt"; + this.module = "Default"; + this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().

Key: The key must be 32 bytes (256 bits) encoded with Base64."; + this.infoURL = "https://asecuritysite.com/encryption/fer"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "string", + "value": "" + }, + ]; + } + /** + * @param {String} input + * @param {Object[]} args + * @returns {String} + */ + run(input, args) { + const [secretInput] = args; + try { + const secret = new fernet.Secret(secretInput); + const token = new fernet.Token({ + secret: secret, + }); + return token.encode(input); + } catch (err) { + throw new OperationError(err); + } + } +} + +export default FernetEncrypt; diff --git a/tests/operations/tests/Fernet.mjs b/tests/operations/tests/Fernet.mjs new file mode 100644 index 00000000..0632fca9 --- /dev/null +++ b/tests/operations/tests/Fernet.mjs @@ -0,0 +1,80 @@ +/** + * Fernet tests. + * + * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @copyright Karsten Silkenbäumer 2019 + * @license Apache-2.0 + */ +import TestRegister from "../TestRegister"; + +TestRegister.addTests([ + { + name: "Fernet Decrypt: no input", + input: "", + expectedOutput: "Error: Invalid version", + recipeConfig: [ + { + op: "Fernet Decrypt", + args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] + } + ], + }, + { + name: "Fernet Decrypt: no secret", + input: "gAAAAABce-Tycae8klRxhDX2uenJ-uwV8-A1XZ2HRnfOXlNzkKKfRxviNLlgtemhT_fd1Fw5P_zFUAjd69zaJBQyWppAxVV00SExe77ql8c5n62HYJOnoIU=", + expectedOutput: "Error: Secret must be 32 url-safe base64-encoded bytes.", + recipeConfig: [ + { + op: "Fernet Decrypt", + args: [""] + } + ], + }, + { + name: "Fernet Decrypt: valid arguments", + input: "gAAAAABce-Tycae8klRxhDX2uenJ-uwV8-A1XZ2HRnfOXlNzkKKfRxviNLlgtemhT_fd1Fw5P_zFUAjd69zaJBQyWppAxVV00SExe77ql8c5n62HYJOnoIU=", + expectedOutput: "This is a secret message.\n", + recipeConfig: [ + { + op: "Fernet Decrypt", + args: ["VGhpc0lzVGhpcnR5VHdvQ2hhcmFjdGVyc0xvbmdLZXk="] + } + ], + } +]); + +TestRegister.addTests([ + { + name: "Fernet Encrypt: no input", + input: "", + expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/, + recipeConfig: [ + { + op: "Fernet Encrypt", + args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] + } + ], + }, + { + name: "Fernet Encrypt: no secret", + input: "This is a secret message.\n", + expectedOutput: "Error: Secret must be 32 url-safe base64-encoded bytes.", + recipeConfig: [ + { + op: "Fernet Encrypt", + args: [""] + } + ], + }, + { + name: "Fernet Encrypt: valid arguments", + input: "This is a secret message.\n", + expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/, + recipeConfig: [ + { + op: "Fernet Encrypt", + args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] + } + ], + } +]); From 55cac174564cf71da857f6aee0941e06635d445d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karsten=20Silkenb=C3=A4umer?= Date: Sun, 3 Mar 2019 17:19:07 +0100 Subject: [PATCH 005/686] Change author URL --- src/core/operations/FernetDecrypt.mjs | 2 +- src/core/operations/FernetEncrypt.mjs | 2 +- tests/operations/tests/Fernet.mjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/operations/FernetDecrypt.mjs b/src/core/operations/FernetDecrypt.mjs index 76d4fd16..d68593d8 100644 --- a/src/core/operations/FernetDecrypt.mjs +++ b/src/core/operations/FernetDecrypt.mjs @@ -1,5 +1,5 @@ /** - * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @author Karsten Silkenbäumer [github.com/kassi] * @copyright Karsten Silkenbäumer 2019 * @license Apache-2.0 */ diff --git a/src/core/operations/FernetEncrypt.mjs b/src/core/operations/FernetEncrypt.mjs index ac8c64cb..2f98449f 100644 --- a/src/core/operations/FernetEncrypt.mjs +++ b/src/core/operations/FernetEncrypt.mjs @@ -1,5 +1,5 @@ /** - * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @author Karsten Silkenbäumer [github.com/kassi] * @copyright Karsten Silkenbäumer 2019 * @license Apache-2.0 */ diff --git a/tests/operations/tests/Fernet.mjs b/tests/operations/tests/Fernet.mjs index 0632fca9..ee9ba2f1 100644 --- a/tests/operations/tests/Fernet.mjs +++ b/tests/operations/tests/Fernet.mjs @@ -1,7 +1,7 @@ /** * Fernet tests. * - * @author Karsten Silkenbäumer [kassi@users.noreply.github.com] + * @author Karsten Silkenbäumer [github.com/kassi] * @copyright Karsten Silkenbäumer 2019 * @license Apache-2.0 */ From be2080259ec9ae7d64945fc5640188ec4b773ba6 Mon Sep 17 00:00:00 2001 From: Kyle Parrish Date: Wed, 2 Oct 2019 09:57:50 -0400 Subject: [PATCH 006/686] Add Fang URL to categories --- src/core/config/Categories.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 94f7fd30..18fc19ff 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -183,6 +183,7 @@ "Encode NetBIOS Name", "Decode NetBIOS Name", "Defang URL", + "Fang URL", "Defang IP Addresses" ] }, From cd15a8c406726bf06d55b879d271ac3f79b3ba99 Mon Sep 17 00:00:00 2001 From: Kyle Parrish Date: Wed, 2 Oct 2019 09:58:28 -0400 Subject: [PATCH 007/686] Create FangURL.mjs --- src/core/operations/FangURL.mjs | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/core/operations/FangURL.mjs diff --git a/src/core/operations/FangURL.mjs b/src/core/operations/FangURL.mjs new file mode 100644 index 00000000..5badaae7 --- /dev/null +++ b/src/core/operations/FangURL.mjs @@ -0,0 +1,77 @@ +/** + * @author arnydo [github@arnydo.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * FangURL operation + */ +class FangURL extends Operation { + + /** + * FangURL constructor + */ + constructor() { + super(); + + this.name = "Fang URL"; + this.module = "Default"; + this.description = "Takes a 'Defanged' Universal Resource Locator (URL) and 'Fangs' it. Meaning, it removes the alterations (defanged) that render it useless so that it can be used again."; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Escape [.]", + type: "boolean", + value: true + }, + { + name: "Escape hxxp", + type: "boolean", + value: true + }, + { + name: "Escape ://", + type: "boolean", + value: true + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [dots, http, slashes] = args; + + input = fangURL(input, dots, http, slashes); + + return input; + } + +} + + +/** + * Defangs a given URL + * + * @param {string} url + * @param {boolean} dots + * @param {boolean} http + * @param {boolean} slashes + * @returns {string} + */ +function fangURL(url, dots, http, slashes) { + if (dots) url = url.replace(/\[\.\]/g, "."); + if (http) url = url.replace(/hxxp/g, "http"); + if (slashes) url = url.replace(/\[\:\/\/\]/g, "://"); + + return url; +} + +export default FangURL; From 794e0effba5ed4193265ddc6429ba55f6dac33d4 Mon Sep 17 00:00:00 2001 From: Alan C Date: Mon, 7 Oct 2019 20:02:28 +0800 Subject: [PATCH 008/686] Add "To Float" and "From Float" operations --- package-lock.json | 6 +- package.json | 1 + src/core/config/Categories.json | 2 + src/core/operations/FromFloat.mjs | 78 ++++++++++++++ src/core/operations/ToFloat.mjs | 80 +++++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Float.mjs | 164 ++++++++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 src/core/operations/FromFloat.mjs create mode 100644 src/core/operations/ToFloat.mjs create mode 100644 tests/operations/tests/Float.mjs diff --git a/package-lock.json b/package-lock.json index 11c80ca0..930dfc40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7657,9 +7657,9 @@ "integrity": "sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==" }, "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", diff --git a/package.json b/package.json index e9c33484..1283f545 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "file-saver": "^2.0.2", "geodesy": "^1.1.3", "highlight.js": "^9.15.10", + "ieee754": "^1.1.13", "jimp": "^0.6.4", "jquery": "3.4.1", "js-crc": "^0.2.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 94f7fd30..939aa22e 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -14,6 +14,8 @@ "From Charcode", "To Decimal", "From Decimal", + "To Float", + "From Float", "To Binary", "From Binary", "To Octal", diff --git a/src/core/operations/FromFloat.mjs b/src/core/operations/FromFloat.mjs new file mode 100644 index 00000000..4fe5990e --- /dev/null +++ b/src/core/operations/FromFloat.mjs @@ -0,0 +1,78 @@ +/** + * @author tcode2k16 [tcode2k16@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import ieee754 from "ieee754"; +import {DELIM_OPTIONS} from "../lib/Delim.mjs"; + +/** + * From Float operation + */ +class FromFloat extends Operation { + + /** + * FromFloat constructor + */ + constructor() { + super(); + + this.name = "From Float"; + this.module = "Default"; + this.description = "Convert from EEE754 Floating Point Numbers"; + this.infoURL = "https://en.wikipedia.org/wiki/IEEE_754"; + this.inputType = "string"; + this.outputType = "byteArray"; + this.args = [ + { + "name": "Endianness", + "type": "option", + "value": [ + "Big Endian", + "Little Endian" + ] + }, + { + "name": "Size", + "type": "option", + "value": [ + "Float (4 bytes)", + "Double (8 bytes)" + ] + }, + { + "name": "Delimiter", + "type": "option", + "value": DELIM_OPTIONS + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + if (input.length === 0) return []; + + const [endianness, size, delimiterName] = args; + const delim = Utils.charRep(delimiterName || "Space"); + const byteSize = size === "Double (8 bytes)" ? 8 : 4; + const isLE = endianness === "Little Endian"; + const mLen = byteSize === 4 ? 23 : 52; + const floats = input.split(delim); + + const output = new Array(floats.length*byteSize); + for (let i = 0; i < floats.length; i++) { + ieee754.write(output, parseFloat(floats[i]), i*byteSize, isLE, mLen, byteSize); + } + return output; + } + +} + +export default FromFloat; diff --git a/src/core/operations/ToFloat.mjs b/src/core/operations/ToFloat.mjs new file mode 100644 index 00000000..b9aef638 --- /dev/null +++ b/src/core/operations/ToFloat.mjs @@ -0,0 +1,80 @@ +/** + * @author tcode2k16 [tcode2k16@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; +import ieee754 from "ieee754"; +import {DELIM_OPTIONS} from "../lib/Delim.mjs"; + +/** + * To Float operation + */ +class ToFloat extends Operation { + + /** + * ToFloat constructor + */ + constructor() { + super(); + + this.name = "To Float"; + this.module = "Default"; + this.description = "Convert to EEE754 Floating Point Numbers"; + this.infoURL = "https://en.wikipedia.org/wiki/IEEE_754"; + this.inputType = "byteArray"; + this.outputType = "string"; + this.args = [ + { + "name": "Endianness", + "type": "option", + "value": [ + "Big Endian", + "Little Endian" + ] + }, + { + "name": "Size", + "type": "option", + "value": [ + "Float (4 bytes)", + "Double (8 bytes)" + ] + }, + { + "name": "Delimiter", + "type": "option", + "value": DELIM_OPTIONS + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [endianness, size, delimiterName] = args; + const delim = Utils.charRep(delimiterName || "Space"); + const byteSize = size === "Double (8 bytes)" ? 8 : 4; + const isLE = endianness === "Little Endian"; + const mLen = byteSize === 4 ? 23 : 52; + + if (input.length % byteSize !== 0) { + throw new OperationError(`Input is not a multiple of ${byteSize}`); + } + + const output = []; + for (let i = 0; i < input.length; i+=byteSize) { + output.push(ieee754.read(input, i, isLE, mLen, byteSize)); + } + return output.join(delim); + } + +} + +export default ToFloat; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 14c7408e..b77f16a9 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -39,6 +39,7 @@ import "./tests/Crypt.mjs"; import "./tests/CSV.mjs"; import "./tests/DateTime.mjs"; import "./tests/ExtractEmailAddresses.mjs"; +import "./tests/Float.mjs"; import "./tests/Fork.mjs"; import "./tests/FromDecimal.mjs"; import "./tests/Hash.mjs"; diff --git a/tests/operations/tests/Float.mjs b/tests/operations/tests/Float.mjs new file mode 100644 index 00000000..3977834c --- /dev/null +++ b/tests/operations/tests/Float.mjs @@ -0,0 +1,164 @@ +/** + * Float tests. + * + * @author tcode2k16 [tcode2k16@gmail.com] + * + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + + +TestRegister.addTests([ + { + name: "To Float: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"] + }, + { + op: "To Float", + args: ["Big Endian", "Float (4 bytes)", "Space"] + } + ], + }, + { + name: "To Float (Big Endian, 4 bytes): 0.5", + input: "3f0000003f000000", + expectedOutput: "0.5 0.5", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"] + }, + { + op: "To Float", + args: ["Big Endian", "Float (4 bytes)", "Space"] + } + ] + }, + { + name: "To Float (Little Endian, 4 bytes): 0.5", + input: "0000003f0000003f", + expectedOutput: "0.5 0.5", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"] + }, + { + op: "To Float", + args: ["Little Endian", "Float (4 bytes)", "Space"] + } + ] + }, + { + name: "To Float (Big Endian, 8 bytes): 0.5", + input: "3fe00000000000003fe0000000000000", + expectedOutput: "0.5 0.5", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"] + }, + { + op: "To Float", + args: ["Big Endian", "Double (8 bytes)", "Space"] + } + ] + }, + { + name: "To Float (Little Endian, 8 bytes): 0.5", + input: "000000000000e03f000000000000e03f", + expectedOutput: "0.5 0.5", + recipeConfig: [ + { + op: "From Hex", + args: ["Auto"] + }, + { + op: "To Float", + args: ["Little Endian", "Double (8 bytes)", "Space"] + } + ] + }, + { + name: "From Float: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Float", + args: ["Big Endian", "Float (4 bytes)", "Space"] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "From Float (Big Endian, 4 bytes): 0.5", + input: "0.5 0.5", + expectedOutput: "3f0000003f000000", + recipeConfig: [ + { + op: "From Float", + args: ["Big Endian", "Float (4 bytes)", "Space"] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "From Float (Little Endian, 4 bytes): 0.5", + input: "0.5 0.5", + expectedOutput: "0000003f0000003f", + recipeConfig: [ + { + op: "From Float", + args: ["Little Endian", "Float (4 bytes)", "Space"] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "From Float (Big Endian, 8 bytes): 0.5", + input: "0.5 0.5", + expectedOutput: "3fe00000000000003fe0000000000000", + recipeConfig: [ + { + op: "From Float", + args: ["Big Endian", "Double (8 bytes)", "Space"] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "From Float (Little Endian, 8 bytes): 0.5", + input: "0.5 0.5", + expectedOutput: "000000000000e03f000000000000e03f", + recipeConfig: [ + { + op: "From Float", + args: ["Little Endian", "Double (8 bytes)", "Space"] + }, + { + op: "To Hex", + args: ["None"] + } + ] + } +]); From 3546ee30a22611f6af16c00532a31eb08fdd2501 Mon Sep 17 00:00:00 2001 From: Kyle Parrish Date: Mon, 7 Oct 2019 16:09:22 -0400 Subject: [PATCH 009/686] Update escaped chars --- src/core/operations/FangURL.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/FangURL.mjs b/src/core/operations/FangURL.mjs index 5badaae7..7390c1a9 100644 --- a/src/core/operations/FangURL.mjs +++ b/src/core/operations/FangURL.mjs @@ -69,7 +69,7 @@ class FangURL extends Operation { function fangURL(url, dots, http, slashes) { if (dots) url = url.replace(/\[\.\]/g, "."); if (http) url = url.replace(/hxxp/g, "http"); - if (slashes) url = url.replace(/\[\:\/\/\]/g, "://"); + if (slashes) url = url.replace(/[://]/g, "://"); return url; } From ce6d38860dcc1b76fc85ecbecde04dbceeb57416 Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 19:39:01 +0100 Subject: [PATCH 010/686] Add operation Added 'Take nth bytes' operation. --- src/core/config/Categories.json | 3 +- src/core/operations/TakeNthBytes.mjs | 78 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/TakeNthBytes.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index db2ab3a6..9fb63d36 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -238,7 +238,8 @@ "Escape string", "Unescape string", "Pseudo-Random Number Generator", - "Sleep" + "Sleep", + "Take nth bytes" ] }, { diff --git a/src/core/operations/TakeNthBytes.mjs b/src/core/operations/TakeNthBytes.mjs new file mode 100644 index 00000000..7dcf9c6e --- /dev/null +++ b/src/core/operations/TakeNthBytes.mjs @@ -0,0 +1,78 @@ +/** + * @author Oshawk [oshawk@protonmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Take nth bytes operation + */ +class TakeNthBytes extends Operation { + + /** + * TakeNthBytes constructor + */ + constructor() { + super(); + + this.name = "Take nth bytes"; + this.module = "Default"; + this.description = "Takes every nth byte starting with a given byte."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.args = [ + { + name: "Take every", + type: "number", + value: 4 + }, + { + name: "Starting at", + type: "number", + value: 0 + }, + { + name: "Apply to each line", + type: "boolean", + value: false + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + let n = args[0]; + let start = args[1]; + let eachLine = args[2]; + + if (parseInt(n) !== n || n <= 0) { + throw new OperationError("'Take every' must be a positive integer.") + } + if (parseInt(start) !== start || start < 0) { + throw new OperationError("'Starting at' must be a positive or zero integer.") + } + + let offset = 0; + let output = []; + for (let i = 0; i < input.length; i++) { + if (eachLine && input[i] == 0x0a) { + offset = i + 1; + } else if (i - offset >= start && (i - (start + offset)) % n == 0) { + output.push(input[i]); + } + } + + return output; + } + +} + +export default TakeNthBytes; From 7c7d1823ca4e31139a804c3ca067806c200c530c Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 19:53:57 +0100 Subject: [PATCH 011/686] Fix formatting issue Fixed issue where new lines were truncated. --- src/core/operations/TakeNthBytes.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/operations/TakeNthBytes.mjs b/src/core/operations/TakeNthBytes.mjs index 7dcf9c6e..ffc42ff9 100644 --- a/src/core/operations/TakeNthBytes.mjs +++ b/src/core/operations/TakeNthBytes.mjs @@ -64,6 +64,7 @@ class TakeNthBytes extends Operation { let output = []; for (let i = 0; i < input.length; i++) { if (eachLine && input[i] == 0x0a) { + output.push(0x0a); offset = i + 1; } else if (i - offset >= start && (i - (start + offset)) % n == 0) { output.push(input[i]); From 502f126986cc10c2eb4df2aca46bcbe98cadeb3a Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 20:15:45 +0100 Subject: [PATCH 012/686] Fix linting Fixed linting. --- src/core/operations/TakeNthBytes.mjs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/operations/TakeNthBytes.mjs b/src/core/operations/TakeNthBytes.mjs index ffc42ff9..05de8869 100644 --- a/src/core/operations/TakeNthBytes.mjs +++ b/src/core/operations/TakeNthBytes.mjs @@ -49,24 +49,24 @@ class TakeNthBytes extends Operation { * @returns {byteArray} */ run(input, args) { - let n = args[0]; - let start = args[1]; - let eachLine = args[2]; + const n = args[0]; + const start = args[1]; + const eachLine = args[2]; - if (parseInt(n) !== n || n <= 0) { - throw new OperationError("'Take every' must be a positive integer.") + if (parseInt(n, 10) !== n || n <= 0) { + throw new OperationError("'Take every' must be a positive integer."); } - if (parseInt(start) !== start || start < 0) { - throw new OperationError("'Starting at' must be a positive or zero integer.") + if (parseInt(start, 10) !== start || start < 0) { + throw new OperationError("'Starting at' must be a positive or zero integer."); } - + let offset = 0; - let output = []; + const output = []; for (let i = 0; i < input.length; i++) { - if (eachLine && input[i] == 0x0a) { + if (eachLine && input[i] === 0x0a) { output.push(0x0a); offset = i + 1; - } else if (i - offset >= start && (i - (start + offset)) % n == 0) { + } else if (i - offset >= start && (i - (start + offset)) % n === 0) { output.push(input[i]); } } From 02f65379736b3a9739342f335fa84476600ea39e Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 20:16:14 +0100 Subject: [PATCH 013/686] Add tests Added tests for the 'Take nth byte' operation. --- tests/operations/tests/TakeNthBytes.mjs | 123 ++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/operations/tests/TakeNthBytes.mjs diff --git a/tests/operations/tests/TakeNthBytes.mjs b/tests/operations/tests/TakeNthBytes.mjs new file mode 100644 index 00000000..22181c3d --- /dev/null +++ b/tests/operations/tests/TakeNthBytes.mjs @@ -0,0 +1,123 @@ +/** + * @author Oshawk [oshawk@protonmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +/** + * Take nth bytes tests + */ +TestRegister.addTests([ + { + name: "Take nth bytes: Nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Take nth bytes: Nothing (apply to each line)", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Take nth bytes: Basic single line", + input: "0123456789", + expectedOutput: "048", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Take nth bytes: Basic single line (apply to each line)", + input: "0123456789", + expectedOutput: "048", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Take nth bytes: Complex single line", + input: "0123456789", + expectedOutput: "59", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 5, false], + }, + ], + }, + { + name: "Take nth bytes: Complex single line (apply to each line)", + input: "0123456789", + expectedOutput: "59", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 5, true], + }, + ], + }, + { + name: "Take nth bytes: Basic multi line", + input: "01234\n56789", + expectedOutput: "047", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Take nth bytes: Basic multi line (apply to each line)", + input: "01234\n56789", + expectedOutput: "04\n59", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Take nth bytes: Complex multi line", + input: "01234\n56789", + expectedOutput: "\n8", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 5, false], + }, + ], + }, + { + name: "Take nth bytes: Complex multi line (apply to each line)", + input: "012345\n6789ab", + expectedOutput: "5\nb", + recipeConfig: [ + { + op: "Take nth bytes", + args: [4, 5, true], + }, + ], + } +]); From 30349dbcb90156b98365462dcd93b6dabb540b9d Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 20:44:57 +0100 Subject: [PATCH 014/686] Add operation Added 'Drop nth bytes' operation. --- src/core/config/Categories.json | 3 +- src/core/operations/DropNthBytes.mjs | 79 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/DropNthBytes.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index db2ab3a6..9cfeb7fe 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -238,7 +238,8 @@ "Escape string", "Unescape string", "Pseudo-Random Number Generator", - "Sleep" + "Sleep", + "Drop nth bytes" ] }, { diff --git a/src/core/operations/DropNthBytes.mjs b/src/core/operations/DropNthBytes.mjs new file mode 100644 index 00000000..e6bac1cd --- /dev/null +++ b/src/core/operations/DropNthBytes.mjs @@ -0,0 +1,79 @@ +/** + * @author Oshawk [oshawk@protonmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Drop nth bytes operation + */ +class DropNthBytes extends Operation { + + /** + * DropNthBytes constructor + */ + constructor() { + super(); + + this.name = "Drop nth bytes"; + this.module = "Default"; + this.description = "Drops every nth byte starting with a given byte."; + this.infoURL = ""; + this.inputType = "byteArray"; + this.outputType = "byteArray"; + this.args = [ + { + name: "Drop every", + type: "number", + value: 4 + }, + { + name: "Starting at", + type: "number", + value: 0 + }, + { + name: "Apply to each line", + type: "boolean", + value: false + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + const n = args[0]; + const start = args[1]; + const eachLine = args[2]; + + if (parseInt(n, 10) !== n || n <= 0) { + throw new OperationError("'Drop every' must be a positive integer."); + } + if (parseInt(start, 10) !== start || start < 0) { + throw new OperationError("'Starting at' must be a positive or zero integer."); + } + + let offset = 0; + const output = []; + for (let i = 0; i < input.length; i++) { + if (eachLine && input[i] === 0x0a) { + output.push(0x0a); + offset = i + 1; + } else if (i - offset < start || (i - (start + offset)) % n !== 0) { + output.push(input[i]); + } + } + + return output; + } + +} + +export default DropNthBytes; From b125f82784274b3d68eb0a1e2777c63bbf68f205 Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 20:59:04 +0100 Subject: [PATCH 015/686] Add tests Added tests for 'Drop nth bytes' operation. --- tests/operations/index.mjs | 1 + tests/operations/tests/DropNthBytes.mjs | 123 ++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 tests/operations/tests/DropNthBytes.mjs diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index d64a7737..046a0b79 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -91,6 +91,7 @@ import "./tests/Protobuf.mjs"; import "./tests/ParseSSHHostKey.mjs"; import "./tests/DefangIP.mjs"; import "./tests/ParseUDP.mjs"; +import "./tests/DropNthBytes.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; diff --git a/tests/operations/tests/DropNthBytes.mjs b/tests/operations/tests/DropNthBytes.mjs new file mode 100644 index 00000000..00d4e0ab --- /dev/null +++ b/tests/operations/tests/DropNthBytes.mjs @@ -0,0 +1,123 @@ +/** + * @author Oshawk [oshawk@protonmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +/** + * Drop nth bytes tests + */ +TestRegister.addTests([ + { + name: "Drop nth bytes: Nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Drop nth bytes: Nothing (apply to each line)", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Drop nth bytes: Basic single line", + input: "0123456789", + expectedOutput: "1235679", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Drop nth bytes: Basic single line (apply to each line)", + input: "0123456789", + expectedOutput: "1235679", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Drop nth bytes: Complex single line", + input: "0123456789", + expectedOutput: "01234678", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 5, false], + }, + ], + }, + { + name: "Drop nth bytes: Complex single line (apply to each line)", + input: "0123456789", + expectedOutput: "01234678", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 5, true], + }, + ], + }, + { + name: "Drop nth bytes: Basic multi line", + input: "01234\n56789", + expectedOutput: "123\n5689", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, false], + }, + ], + }, + { + name: "Drop nth bytes: Basic multi line (apply to each line)", + input: "01234\n56789", + expectedOutput: "123\n678", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 0, true], + }, + ], + }, + { + name: "Drop nth bytes: Complex multi line", + input: "01234\n56789", + expectedOutput: "012345679", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 5, false], + }, + ], + }, + { + name: "Drop nth bytes: Complex multi line (apply to each line)", + input: "012345\n6789ab", + expectedOutput: "01234\n6789a", + recipeConfig: [ + { + op: "Drop nth bytes", + args: [4, 5, true], + }, + ], + } +]); From 518b33643198ed6ee474b9296fa6f7ffdd6f9dfb Mon Sep 17 00:00:00 2001 From: Oshawk Date: Mon, 21 Oct 2019 21:01:33 +0100 Subject: [PATCH 016/686] Add tests to index Added tests to index. --- tests/operations/index.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index d64a7737..348e8460 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -91,6 +91,7 @@ import "./tests/Protobuf.mjs"; import "./tests/ParseSSHHostKey.mjs"; import "./tests/DefangIP.mjs"; import "./tests/ParseUDP.mjs"; +import "./tests/TakeNthBytes.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; From e92ed13864d5e404fa9986e6972b61ca83a7123a Mon Sep 17 00:00:00 2001 From: n1073645 Date: Thu, 21 Nov 2019 12:53:44 +0000 Subject: [PATCH 017/686] PLIST viewer. --- src/core/operations/PLISTViewer.mjs | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/core/operations/PLISTViewer.mjs diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs new file mode 100644 index 00000000..1d263468 --- /dev/null +++ b/src/core/operations/PLISTViewer.mjs @@ -0,0 +1,56 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * PLIST Viewer operation + */ +class PLISTViewer extends Operation { + + /** + * PLISTViewer constructor + */ + constructor() { + super(); + + this.name = "PLIST Viewer"; + this.module = "Other"; + this.description = "Converts PLISTXML file into a human readable format."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + /* Example arguments. See the project wiki for full details. + { + name: "First arg", + type: "string", + value: "Don't Panic" + }, + { + name: "Second arg", + type: "number", + value: 42 + } + */ + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // const [firstArg, secondArg] = args; + + throw new OperationError("Test"); + } + +} + +export default PLISTViewer; From 63bb19d48d06c8e780ba402f2abb0274e0ecc250 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Fri, 22 Nov 2019 08:32:46 +0000 Subject: [PATCH 018/686] Began implementing the PLIST viewer operation --- src/core/config/Categories.json | 1 + src/core/operations/PLISTViewer.mjs | 111 +++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index f663e16d..11e8f076 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -422,6 +422,7 @@ "Frequency distribution", "Index of Coincidence", "Chi Square", + "PLIST Viewer", "Disassemble x86", "Pseudo-Random Number Generator", "Generate UUID", diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs index 1d263468..6229d336 100644 --- a/src/core/operations/PLISTViewer.mjs +++ b/src/core/operations/PLISTViewer.mjs @@ -5,7 +5,6 @@ */ import Operation from "../Operation.mjs"; -import OperationError from "../errors/OperationError.mjs"; /** * PLIST Viewer operation @@ -46,11 +45,115 @@ class PLISTViewer extends Operation { * @returns {string} */ run(input, args) { - // const [firstArg, secondArg] = args; - throw new OperationError("Test"); + const reserved = [["","",8], + ["","",6], + ["","",9], + ["","", 6], + ["","",6], + ["","",7], + ["","",6], + ["","",5], + ["",false,8], + ["",true,7]]; + + function the_viewer(input, dictionary_flag){ + var new_dict = new Array(); + var result = new Array(); + var new_key = null; + while(dictionary_flag ? input.slice(0,7) != "" : input.slice(0,8) != ""){ + reserved.forEach( function (elem, index){ + var element = elem[0]; + var endelement = elem[1]; + var length = elem[2]; + let temp = input.slice(0,length); + if(temp == element){ + input = input.slice(length); + if(temp == ""){ + var returned = the_viewer(input, true); + input = returned[1]; + if(new_key) + new_dict[new_key] = returned[0]; + else + new_dict["plist"] = returned[0]; + new_key = null; + }else if(temp == ""){ + var returned = the_viewer(input, false); + if(dictionary_flag) + new_dict[new_key] = returned[0]; + else + result.push(returned[0]); + input = returned[1]; + new_key = null; + }else if(temp == ""){ + var end = input.indexOf(endelement); + new_key = input.slice(0, end); + input = input.slice(end+length+1); + }else if(temp == "" || temp == ""){ + new_dict[new_key] = endelement; + new_key = null; + }else{ + var end = input.indexOf(endelement); + var toadd = input.slice(0, end); + if(temp == "") + toadd = parseInt(toadd); + else if(temp == "") + toadd = parseFloat(toadd); + if(dictionary_flag){ + new_dict[new_key] = toadd; + new_key = null; + }else{ + result.push(toadd); + } + input = input.slice(end+length+1); + } + } + }); + } + if(dictionary_flag){ + input = input.slice(7); + return [new_dict, input]; + }else{ + input = input.slice(8); + return [result, input]; + } + } + + let result = ""; + function print_it(input, depth) { + Object.keys(input).forEach((key, index) => { + if(typeof(input[key]) == "object") { + result += (("\t".repeat(depth)) + key + ": {\n"); + print_it(input[key], depth+1); + result += (("\t".repeat(depth)) + "}\n"); + } else { + result += (("\t".repeat(depth)) + key + " : " + input[key] + "\n"); + } + }); + } + + while (input.indexOf("/, ""); + } + while (input.indexOf("") !== -1){ + input = input.replace(/<\/plist>/, ""); + } + console.log(input); + while(input.indexOf("\n") !== -1) + input = input.replace("\n", ""); + while(input.indexOf("\t") !== -1) + input = input.replace("\t", ""); + while(input.indexOf(" ") !== -1) + input = input.replace(" ", ""); + console.log(input); + input = input.slice(input.indexOf("")+6); + //return input + var other = the_viewer(input, 1); + print_it(other[0],1); + result = "{\n" + result; + result += "}"; + return result; } - } export default PLISTViewer; From 8e1e1d56cadbb1465a323adec3ac544e9c53f3af Mon Sep 17 00:00:00 2001 From: n1073645 Date: Fri, 22 Nov 2019 15:39:43 +0000 Subject: [PATCH 019/686] Plist viewer operation added. --- src/core/operations/PLISTViewer.mjs | 156 ++++++++++------------------ 1 file changed, 55 insertions(+), 101 deletions(-) diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs index 6229d336..939b7d1a 100644 --- a/src/core/operations/PLISTViewer.mjs +++ b/src/core/operations/PLISTViewer.mjs @@ -46,112 +46,66 @@ class PLISTViewer extends Operation { */ run(input, args) { - const reserved = [["","",8], - ["","",6], - ["","",9], - ["","", 6], - ["","",6], - ["","",7], - ["","",6], - ["","",5], - ["",false,8], - ["",true,7]]; - - function the_viewer(input, dictionary_flag){ - var new_dict = new Array(); - var result = new Array(); - var new_key = null; - while(dictionary_flag ? input.slice(0,7) != "" : input.slice(0,8) != ""){ - reserved.forEach( function (elem, index){ - var element = elem[0]; - var endelement = elem[1]; - var length = elem[2]; - let temp = input.slice(0,length); - if(temp == element){ - input = input.slice(length); - if(temp == ""){ - var returned = the_viewer(input, true); - input = returned[1]; - if(new_key) - new_dict[new_key] = returned[0]; - else - new_dict["plist"] = returned[0]; - new_key = null; - }else if(temp == ""){ - var returned = the_viewer(input, false); - if(dictionary_flag) - new_dict[new_key] = returned[0]; - else - result.push(returned[0]); - input = returned[1]; - new_key = null; - }else if(temp == ""){ - var end = input.indexOf(endelement); - new_key = input.slice(0, end); - input = input.slice(end+length+1); - }else if(temp == "" || temp == ""){ - new_dict[new_key] = endelement; - new_key = null; - }else{ - var end = input.indexOf(endelement); - var toadd = input.slice(0, end); - if(temp == "") - toadd = parseInt(toadd); - else if(temp == "") - toadd = parseFloat(toadd); - if(dictionary_flag){ - new_dict[new_key] = toadd; - new_key = null; - }else{ - result.push(toadd); - } - input = input.slice(end+length+1); - } - } - }); - } - if(dictionary_flag){ - input = input.slice(7); - return [new_dict, input]; - }else{ - input = input.slice(8); - return [result, input]; - } - } - + // Regexes are designed to transform the xml format into a reasonably more readable string format. + input = input.slice(input.indexOf("/g, "plist => ") + .replace(//g, "{") + .replace(/<\/dict>/g, "}") + .replace(//g, "[") + .replace(/<\/array>/g, "]") + .replace(/.+<\/key>/g, m => `${m.slice(5, m.indexOf(/<\/key>/g)-5)}\t=> `) + .replace(/.+<\/real>/g, m => `${m.slice(6, m.indexOf(/<\/real>/g)-6)}\n`) + .replace(/.+<\/string>/g, m => `${m.slice(8, m.indexOf(/<\/string>/g)-8)}\n`) + .replace(/.+<\/integer>/g, m => `${m.slice(9, m.indexOf(/<\/integer>/g)-9)}\n`) + .replace(//g, m => "false") + .replace(//g, m => "true") + .replace(/<\/plist>/g, "/plist") + .replace(/.+<\/date>/g, m => `${m.slice(6, m.indexOf(/<\/integer>/g)-6)}`) + .replace(/(\s|.)+?<\/data>/g, m => `${m.slice(6, m.indexOf(/<\/data>/g)-6)}`) + .replace(/[ \t\r\f\v]/g, ""); + let result = ""; - function print_it(input, depth) { - Object.keys(input).forEach((key, index) => { - if(typeof(input[key]) == "object") { - result += (("\t".repeat(depth)) + key + ": {\n"); - print_it(input[key], depth+1); - result += (("\t".repeat(depth)) + "}\n"); + + /** + * Formats the input after the regex has replaced all of the relevant parts. + * + * @param {array} input + * @param {number} depthCount + */ + function printIt(input, depthCount) { + if (!(input.length)) + return; + + // If the current position points at a larger dynamic structure. + if (input[0].indexOf("=>") !== -1) { + + // If the LHS also points at a larger structure (nested plists in a dictionary). + if (input[1].indexOf("=>") !== -1) { + result += ("\t".repeat(depthCount)) + input[0].slice(0, -2) + " => " + input[1].slice(0, -2) + " =>\n"; } else { - result += (("\t".repeat(depth)) + key + " : " + input[key] + "\n"); + result += ("\t".repeat(depthCount)) + input[0].slice(0, -2) + " => " + input[1] + "\n"; } - }); + + // Controls the tab depth for how many opening braces there have been. + if (input[1] === "{" || input[1] === "[") { + depthCount += 1; + } + input = input.slice(1); + } else { + // Controls the tab depth for how many closing braces there have been. + if (input[0] === "}" || input[0] === "]") + depthCount--; + + // Has to be here since the formatting breaks otherwise. + result += ("\t".repeat(depthCount)) + input[0] + "\n"; + if (input[0] === "{" || input[0] === "[") + depthCount++; + } + printIt(input.slice(1), depthCount); } - while (input.indexOf("/, ""); - } - while (input.indexOf("") !== -1){ - input = input.replace(/<\/plist>/, ""); - } - console.log(input); - while(input.indexOf("\n") !== -1) - input = input.replace("\n", ""); - while(input.indexOf("\t") !== -1) - input = input.replace("\t", ""); - while(input.indexOf(" ") !== -1) - input = input.replace(" ", ""); - console.log(input); - input = input.slice(input.indexOf("")+6); - //return input - var other = the_viewer(input, 1); - print_it(other[0],1); - result = "{\n" + result; - result += "}"; + input = input.split("\n").filter(e => e !== ""); + printIt(input, 0); return result; } } From 0295d0c9b47d6cd6b30492ce3b77b3741414cde9 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 25 Nov 2019 10:35:45 +0000 Subject: [PATCH 020/686] Tided up presentation of the PLIST --- src/core/operations/PLISTViewer.mjs | 71 +++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs index 939b7d1a..8232fb14 100644 --- a/src/core/operations/PLISTViewer.mjs +++ b/src/core/operations/PLISTViewer.mjs @@ -55,7 +55,7 @@ class PLISTViewer extends Operation { .replace(/<\/array>/g, "]") .replace(/.+<\/key>/g, m => `${m.slice(5, m.indexOf(/<\/key>/g)-5)}\t=> `) .replace(/.+<\/real>/g, m => `${m.slice(6, m.indexOf(/<\/real>/g)-6)}\n`) - .replace(/.+<\/string>/g, m => `${m.slice(8, m.indexOf(/<\/string>/g)-8)}\n`) + .replace(/.+<\/string>/g, m => `"${m.slice(8, m.indexOf(/<\/string>/g)-8)}"\n`) .replace(/.+<\/integer>/g, m => `${m.slice(9, m.indexOf(/<\/integer>/g)-9)}\n`) .replace(//g, m => "false") .replace(//g, m => "true") @@ -64,44 +64,77 @@ class PLISTViewer extends Operation { .replace(/(\s|.)+?<\/data>/g, m => `${m.slice(6, m.indexOf(/<\/data>/g)-6)}`) .replace(/[ \t\r\f\v]/g, ""); + /** + * Depending on the type of brace, it will increment the depth and amount of arrays accordingly. + * + * @param {string} elem + * @param {array} vals + * @param {number} offset + */ + function braces(elem, vals,offset) { + let temp = vals.indexOf(elem); + if (temp !== -1) { + depthCount += offset; + if (temp === 1) + arrCount += offset; + } + } + let result = ""; + let arrCount = 0; + let depthCount = 0; /** * Formats the input after the regex has replaced all of the relevant parts. * * @param {array} input - * @param {number} depthCount + * @param {number} index */ - function printIt(input, depthCount) { + function printIt(input, index) { if (!(input.length)) return; + let temp = ""; + const origArr = arrCount; + let currElem = input[0]; + // If the current position points at a larger dynamic structure. - if (input[0].indexOf("=>") !== -1) { + if (currElem.indexOf("=>") !== -1) { // If the LHS also points at a larger structure (nested plists in a dictionary). - if (input[1].indexOf("=>") !== -1) { - result += ("\t".repeat(depthCount)) + input[0].slice(0, -2) + " => " + input[1].slice(0, -2) + " =>\n"; - } else { - result += ("\t".repeat(depthCount)) + input[0].slice(0, -2) + " => " + input[1] + "\n"; - } + if (input[1].indexOf("=>") !== -1) + temp = currElem.slice(0, -2) + " => " + input[1].slice(0, -2) + " =>\n"; + else + temp = currElem.slice(0, -2) + " => " + input[1] + "\n"; - // Controls the tab depth for how many opening braces there have been. - if (input[1] === "{" || input[1] === "[") { - depthCount += 1; - } input = input.slice(1); } else { // Controls the tab depth for how many closing braces there have been. - if (input[0] === "}" || input[0] === "]") - depthCount--; + + braces(currElem, ["}", "]"], -1); // Has to be here since the formatting breaks otherwise. - result += ("\t".repeat(depthCount)) + input[0] + "\n"; - if (input[0] === "{" || input[0] === "[") - depthCount++; + temp = currElem + "\n"; } - printIt(input.slice(1), depthCount); + + currElem = input[0]; + + // Tab out to the correct distance. + result += ("\t".repeat(depthCount)); + + // If it is enclosed in an array show index. + if (arrCount > 0 && currElem !== "]") + result += index.toString() + " => "; + + result += temp; + + // Controls the tab depth for how many opening braces there have been. + braces(currElem, ["{", "["],1); + + // If there has been a new array then reset index. + if (arrCount > origArr) + return printIt(input.slice(1), 0); + return printIt(input.slice(1), ++index); } input = input.split("\n").filter(e => e !== ""); From d8405e5f814e17319dd293fdcddfdeeca2a43f15 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 25 Nov 2019 10:37:30 +0000 Subject: [PATCH 021/686] Linting on PLIST viewer operation. --- src/core/operations/PLISTViewer.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs index 8232fb14..b8a90c5b 100644 --- a/src/core/operations/PLISTViewer.mjs +++ b/src/core/operations/PLISTViewer.mjs @@ -71,8 +71,8 @@ class PLISTViewer extends Operation { * @param {array} vals * @param {number} offset */ - function braces(elem, vals,offset) { - let temp = vals.indexOf(elem); + function braces(elem, vals, offset) { + const temp = vals.indexOf(elem); if (temp !== -1) { depthCount += offset; if (temp === 1) @@ -129,7 +129,7 @@ class PLISTViewer extends Operation { result += temp; // Controls the tab depth for how many opening braces there have been. - braces(currElem, ["{", "["],1); + braces(currElem, ["{", "["], 1); // If there has been a new array then reset index. if (arrCount > origArr) From c689cf7f134df8e8309302c88e8b9bf1a22e94f8 Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Thu, 9 Jan 2020 15:14:33 +0000 Subject: [PATCH 022/686] Fix #930 by allowing variable key sizes --- src/core/operations/BlowfishDecrypt.mjs | 8 ++++++-- src/core/operations/BlowfishEncrypt.mjs | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/operations/BlowfishDecrypt.mjs b/src/core/operations/BlowfishDecrypt.mjs index 07b6a0ff..83236327 100644 --- a/src/core/operations/BlowfishDecrypt.mjs +++ b/src/core/operations/BlowfishDecrypt.mjs @@ -70,10 +70,14 @@ class BlowfishDecrypt extends Operation { inputType = args[3], outputType = args[4]; - if (key.length !== 8) { + if (key.length < 4 || key.length > 56) { throw new OperationError(`Invalid key length: ${key.length} bytes -Blowfish uses a key length of 8 bytes (64 bits).`); +Blowfish's key length needs to between 4 and 56 bytes (32-448 bits).`); + } + + if (iv.length !== 8) { + throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`); } input = Utils.convertToByteString(input, inputType); diff --git a/src/core/operations/BlowfishEncrypt.mjs b/src/core/operations/BlowfishEncrypt.mjs index e7e558cd..ebf5e5c2 100644 --- a/src/core/operations/BlowfishEncrypt.mjs +++ b/src/core/operations/BlowfishEncrypt.mjs @@ -70,10 +70,14 @@ class BlowfishEncrypt extends Operation { inputType = args[3], outputType = args[4]; - if (key.length !== 8) { + if (key.length < 4 || key.length > 56) { throw new OperationError(`Invalid key length: ${key.length} bytes + +Blowfish's key length needs to between 4 and 56 bytes (32-448 bits).`); + } -Blowfish uses a key length of 8 bytes (64 bits).`); + if (iv.length !== 8) { + throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`); } input = Utils.convertToByteString(input, inputType); From 9e17825b53b371ed1c8671472ef6585f96a29d86 Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Thu, 9 Jan 2020 15:15:01 +0000 Subject: [PATCH 023/686] Add variable key size tests --- tests/operations/tests/Crypt.mjs | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/operations/tests/Crypt.mjs b/tests/operations/tests/Crypt.mjs index b56f9cf8..a6b9e2ac 100644 --- a/tests/operations/tests/Crypt.mjs +++ b/tests/operations/tests/Crypt.mjs @@ -1751,4 +1751,38 @@ DES uses a key length of 8 bytes (64 bits).`, } ], }, + { + name: "Blowfish Encrypt with variable key length: CBC, ASCII, 4 bytes", + input: "The quick brown fox jumps over the lazy dog.", + expectedOutput: "823f337a53ecf121aa9ec1b111bd5064d1d7586abbdaaa0c8fd0c6cc43c831c88bf088ee3e07287e3f36cf2e45f9c7e6", + recipeConfig: [ + { + "op": "Blowfish Encrypt", + "args": [ + {"option": "Hex", "string": "00112233"}, // Key + {"option": "Hex", "string": "0000000000000000"}, // IV + "CBC", // Mode + "Raw", // Input + "Hex" // Output + ] + } + ], + }, + { + name: "Blowfish Encrypt with variable key length: CBC, ASCII, 42 bytes", + input: "The quick brown fox jumps over the lazy dog.", + expectedOutput: "19f5a68145b34321cfba72226b0f33922ce44dd6e7869fe328db64faae156471216f12ed2a37fd0bdd7cebf867b3cff0", + recipeConfig: [ + { + "op": "Blowfish Encrypt", + "args": [ + {"option": "Hex", "string": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead"}, // Key + {"option": "Hex", "string": "0000000000000000"}, // IV + "CBC", // Mode + "Raw", // Input + "Hex" // Output + ] + } + ], + } ]); From 81605b2222e2a4b9b41198651da3abc9f2156082 Mon Sep 17 00:00:00 2001 From: Andy Wang Date: Sat, 11 Jan 2020 10:47:40 +0000 Subject: [PATCH 024/686] Grammar typo --- src/core/operations/BlowfishDecrypt.mjs | 2 +- src/core/operations/BlowfishEncrypt.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/BlowfishDecrypt.mjs b/src/core/operations/BlowfishDecrypt.mjs index 83236327..a80fdb2b 100644 --- a/src/core/operations/BlowfishDecrypt.mjs +++ b/src/core/operations/BlowfishDecrypt.mjs @@ -73,7 +73,7 @@ class BlowfishDecrypt extends Operation { if (key.length < 4 || key.length > 56) { throw new OperationError(`Invalid key length: ${key.length} bytes -Blowfish's key length needs to between 4 and 56 bytes (32-448 bits).`); +Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`); } if (iv.length !== 8) { diff --git a/src/core/operations/BlowfishEncrypt.mjs b/src/core/operations/BlowfishEncrypt.mjs index ebf5e5c2..7d550d46 100644 --- a/src/core/operations/BlowfishEncrypt.mjs +++ b/src/core/operations/BlowfishEncrypt.mjs @@ -73,7 +73,7 @@ class BlowfishEncrypt extends Operation { if (key.length < 4 || key.length > 56) { throw new OperationError(`Invalid key length: ${key.length} bytes -Blowfish's key length needs to between 4 and 56 bytes (32-448 bits).`); +Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`); } if (iv.length !== 8) { From 0259ed8314c0635124d7be316f9e9ec583f4cce0 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 27 Jan 2020 16:07:54 +0000 Subject: [PATCH 025/686] LS47 implemented, needs linting --- src/core/lib/LS47.mjs | 148 ++++++++++++++++++++++++++++ src/core/operations/LS47Decrypt.mjs | 58 +++++++++++ src/core/operations/LS47Encrypt.mjs | 63 ++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 src/core/lib/LS47.mjs create mode 100644 src/core/operations/LS47Decrypt.mjs create mode 100644 src/core/operations/LS47Encrypt.mjs diff --git a/src/core/lib/LS47.mjs b/src/core/lib/LS47.mjs new file mode 100644 index 00000000..a4ef10a5 --- /dev/null +++ b/src/core/lib/LS47.mjs @@ -0,0 +1,148 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError.mjs"; + +let letters = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"; +let tiles = []; + +export function init_tiles() { + for (let i = 0; i < 49; i++) + tiles.push([letters.charAt(i), [Math.floor(i/7), i % 7]]); +} + +function rotate_down(key, col, n) { + let lines = []; + for (let i = 0; i < 7; i++) + lines.push(key.slice(i*7, (i + 1) * 7)); + let lefts = []; + let mids = []; + let rights = []; + lines.forEach((element) => { + lefts.push(element.slice(0, col)); + mids.push(element.charAt(col)); + rights.push(element.slice(col+1)); + }); + n = (7 - n % 7) % 7; + mids = mids.slice(n).concat(mids.slice(0, n)); + let result = ""; + for (let i = 0; i < 7; i++) + result += lefts[i] + mids[i] + rights[i]; + return result; +} + +function rotate_right(key, row, n) { + let mid = key.slice(row * 7, (row + 1) * 7); + n = (7 - n % 7) % 7; + return key.slice(0, 7 * row) + mid.slice(n) + mid.slice(0, n) + key.slice(7 * (row + 1)); +} + +function find_ix(letter) { + for (let i = 0; i < tiles.length; i++) + if (tiles[i][0] === letter) + return tiles[i][1]; + throw new OperationError("Letter " + letter + " is not included in LS47"); +} + +export function derive_key(password) { + let i = 0; + let k = letters; + for (const c of password) { + let [row, col] = find_ix(c); + k = rotate_down(rotate_right(k, i, col), i, row); + i = (i + 1) % 7; + } + return k; +} + +function check_key(key) { + if (key.length !== letters.length) + throw new OperationError("Wrong key size"); + let counts = new Array(); + for (let i = 0; i < letters.length; i++) + counts[letters.charAt(i)] = 0; + for (const elem of letters){ + if (letters.indexOf(elem) === -1) + throw new OperationError("Letter " + elem + " not in LS47!"); + counts[elem]++; + if (counts[elem] > 1) + throw new OperationError("Letter duplicated in the key!"); + } +} + +function find_pos (key, letter) { + let index = key.indexOf(letter); + if (index >= 0 && index < 49) + return [Math.floor(index/7), index%7]; + throw new OperationError("Letter " + letter + " is not in the key!"); +} + +function find_at_pos(key, coord) { + return key.charAt(coord[1] + (coord[0] * 7)); +} + +function add_pos(a, b) { + return [(a[0] + b[0]) % 7, (a[1] + b[1]) % 7]; +} + +function sub_pos(a, b) { + let asub = a[0] - b[0]; + let bsub = a[1] - b[1]; + return [asub - (Math.floor(asub/7) * 7), bsub - (Math.floor(bsub/7) * 7)]; +} + +function encrypt(key, plaintext) { + check_key(key); + let mp = [0, 0]; + let ciphertext = ''; + for (const p of plaintext) { + let pp = find_pos(key, p); + let mix = find_ix(find_at_pos(key, mp)); + let cp = add_pos(pp, mix); + let c = find_at_pos(key, cp); + ciphertext += c; + key = rotate_right(key, pp[0], 1); + cp = find_pos(key, c); + key = rotate_down(key, cp[1], 1); + mp = add_pos(mp, find_ix(c)); + } + return ciphertext; +} + +function decrypt(key, ciphertext) { + check_key(key); + let mp = [0,0]; + let plaintext = ''; + for (const c of ciphertext) { + let cp = find_pos(key, c); + let mix = find_ix(find_at_pos(key, mp)); + let pp = sub_pos(cp, mix); + let p = find_at_pos(key, pp); + + plaintext += p; + key = rotate_right(key, pp[0], 1); + cp = find_pos(key, c); + key = rotate_down(key, cp[1], 1); + mp = add_pos(mp, find_ix(c)); + } + return plaintext; +} + +export function encrypt_pad(key, plaintext, signature, padding_size) { + init_tiles(); + check_key(key); + let padding = ""; + for (let i = 0; i < padding_size; i++) { + padding += letters.charAt(Math.floor(Math.random() * letters.length)); + } + return encrypt(key, padding+plaintext+'---'+signature); +} + +export function decrypt_pad(key, ciphertext, padding_size) { + init_tiles(); + check_key(key); + return decrypt(key, ciphertext).slice(padding_size); +} \ No newline at end of file diff --git a/src/core/operations/LS47Decrypt.mjs b/src/core/operations/LS47Decrypt.mjs new file mode 100644 index 00000000..ffda8f93 --- /dev/null +++ b/src/core/operations/LS47Decrypt.mjs @@ -0,0 +1,58 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import * as LS47 from "../lib/LS47.mjs" + +/** + * LS47 Decrypt operation + */ +class LS47Decrypt extends Operation { + + /** + * LS47Decrypt constructor + */ + constructor() { + super(); + + this.name = "LS47 Decrypt"; + this.module = "Crypto"; + this.description = ""; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Password", + type: "string", + value: "" + }, + { + name: "Padding", + type: "number", + value: 10 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + + this.padding_size = parseInt(args[1], 10); + + LS47.init_tiles(); + + let key = LS47.derive_key(args[0]); + return LS47.decrypt_pad(key, input, this.padding_size); + } + +} + +export default LS47Decrypt; diff --git a/src/core/operations/LS47Encrypt.mjs b/src/core/operations/LS47Encrypt.mjs new file mode 100644 index 00000000..bf3b0306 --- /dev/null +++ b/src/core/operations/LS47Encrypt.mjs @@ -0,0 +1,63 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import * as LS47 from "../lib/LS47.mjs" + +/** + * LS47 Encrypt operation + */ +class LS47Encrypt extends Operation { + + /** + * LS47Encrypt constructor + */ + constructor() { + super(); + + this.name = "LS47 Encrypt"; + this.module = "Crypto"; + this.description = ""; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Password", + type: "string", + value: "" + }, + { + name: "Padding", + type: "number", + value: 10 + }, + { + name: "Signature", + type: "string", + value: "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + + this.padding_size = parseInt(args[1], 10); + + LS47.init_tiles(); + + let key = LS47.derive_key(args[0]); + return LS47.encrypt_pad(key, input, args[2], this.padding_size); + } + +} + +export default LS47Encrypt; From 5cdd062ed9c639bf387c783667d8bd86302e8acb Mon Sep 17 00:00:00 2001 From: n1073645 Date: Tue, 28 Jan 2020 09:33:32 +0000 Subject: [PATCH 026/686] Linting done --- src/core/config/Categories.json | 2 + src/core/lib/LS47.mjs | 153 ++++++++++++++++++---------- src/core/operations/LS47Decrypt.mjs | 12 +-- src/core/operations/LS47Encrypt.mjs | 14 +-- 4 files changed, 112 insertions(+), 69 deletions(-) diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 53ca796d..1b810d37 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -75,6 +75,8 @@ "DES Decrypt", "Triple DES Encrypt", "Triple DES Decrypt", + "LS47 Encrypt", + "LS47 Decrypt", "RC2 Encrypt", "RC2 Decrypt", "RC4", diff --git a/src/core/lib/LS47.mjs b/src/core/lib/LS47.mjs index a4ef10a5..b028fc4f 100644 --- a/src/core/lib/LS47.mjs +++ b/src/core/lib/LS47.mjs @@ -6,21 +6,27 @@ import OperationError from "../errors/OperationError.mjs"; -let letters = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"; -let tiles = []; +const letters = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"; +const tiles = []; -export function init_tiles() { +/** + * + */ +export function initTiles() { for (let i = 0; i < 49; i++) tiles.push([letters.charAt(i), [Math.floor(i/7), i % 7]]); } -function rotate_down(key, col, n) { - let lines = []; - for (let i = 0; i < 7; i++) +/** + * + */ +function rotateDown(key, col, n) { + const lines = []; + for (let i = 0; i < 7; i++) lines.push(key.slice(i*7, (i + 1) * 7)); - let lefts = []; + const lefts = []; let mids = []; - let rights = []; + const rights = []; lines.forEach((element) => { lefts.push(element.slice(0, col)); mids.push(element.charAt(col)); @@ -34,37 +40,49 @@ function rotate_down(key, col, n) { return result; } -function rotate_right(key, row, n) { - let mid = key.slice(row * 7, (row + 1) * 7); +/** + * + */ +function rotateRight(key, row, n) { + const mid = key.slice(row * 7, (row + 1) * 7); n = (7 - n % 7) % 7; return key.slice(0, 7 * row) + mid.slice(n) + mid.slice(0, n) + key.slice(7 * (row + 1)); } -function find_ix(letter) { +/** + * + */ +function findIx(letter) { for (let i = 0; i < tiles.length; i++) if (tiles[i][0] === letter) return tiles[i][1]; throw new OperationError("Letter " + letter + " is not included in LS47"); } -export function derive_key(password) { +/** + * + */ +export function deriveKey(password) { let i = 0; let k = letters; for (const c of password) { - let [row, col] = find_ix(c); - k = rotate_down(rotate_right(k, i, col), i, row); + const [row, col] = findIx(c); + k = rotateDown(rotateRight(k, i, col), i, row); i = (i + 1) % 7; } return k; } -function check_key(key) { +/** + * + */ +function checkKey(key) { if (key.length !== letters.length) throw new OperationError("Wrong key size"); - let counts = new Array(); + const counts = new Array(); for (let i = 0; i < letters.length; i++) counts[letters.charAt(i)] = 0; - for (const elem of letters){ + for (const elem of letters) { if (letters.indexOf(elem) === -1) throw new OperationError("Letter " + elem + " not in LS47!"); counts[elem]++; @@ -73,76 +91,99 @@ function check_key(key) { } } -function find_pos (key, letter) { - let index = key.indexOf(letter); +/** + * + */ +function findPos (key, letter) { + const index = key.indexOf(letter); if (index >= 0 && index < 49) return [Math.floor(index/7), index%7]; throw new OperationError("Letter " + letter + " is not in the key!"); } -function find_at_pos(key, coord) { +/** + * + */ +function findAtPos(key, coord) { return key.charAt(coord[1] + (coord[0] * 7)); } -function add_pos(a, b) { +/** + * + */ +function addPos(a, b) { return [(a[0] + b[0]) % 7, (a[1] + b[1]) % 7]; } -function sub_pos(a, b) { - let asub = a[0] - b[0]; - let bsub = a[1] - b[1]; +/** + * + */ +function subPos(a, b) { + const asub = a[0] - b[0]; + const bsub = a[1] - b[1]; return [asub - (Math.floor(asub/7) * 7), bsub - (Math.floor(bsub/7) * 7)]; } +/** + * + */ function encrypt(key, plaintext) { - check_key(key); + checkKey(key); let mp = [0, 0]; - let ciphertext = ''; + let ciphertext = ""; for (const p of plaintext) { - let pp = find_pos(key, p); - let mix = find_ix(find_at_pos(key, mp)); - let cp = add_pos(pp, mix); - let c = find_at_pos(key, cp); + const pp = findPos(key, p); + const mix = findIx(findAtPos(key, mp)); + let cp = addPos(pp, mix); + const c = findAtPos(key, cp); ciphertext += c; - key = rotate_right(key, pp[0], 1); - cp = find_pos(key, c); - key = rotate_down(key, cp[1], 1); - mp = add_pos(mp, find_ix(c)); + key = rotateRight(key, pp[0], 1); + cp = findPos(key, c); + key = rotateDown(key, cp[1], 1); + mp = addPos(mp, findIx(c)); } return ciphertext; } +/** + * + */ function decrypt(key, ciphertext) { - check_key(key); - let mp = [0,0]; - let plaintext = ''; + checkKey(key); + let mp = [0, 0]; + let plaintext = ""; for (const c of ciphertext) { - let cp = find_pos(key, c); - let mix = find_ix(find_at_pos(key, mp)); - let pp = sub_pos(cp, mix); - let p = find_at_pos(key, pp); - + let cp = findPos(key, c); + const mix = findIx(findAtPos(key, mp)); + const pp = subPos(cp, mix); + const p = findAtPos(key, pp); plaintext += p; - key = rotate_right(key, pp[0], 1); - cp = find_pos(key, c); - key = rotate_down(key, cp[1], 1); - mp = add_pos(mp, find_ix(c)); + key = rotateRight(key, pp[0], 1); + cp = findPos(key, c); + key = rotateDown(key, cp[1], 1); + mp = addPos(mp, findIx(c)); } return plaintext; } -export function encrypt_pad(key, plaintext, signature, padding_size) { - init_tiles(); - check_key(key); +/** + * + */ +export function encryptPad(key, plaintext, signature, paddingSize) { + initTiles(); + checkKey(key); let padding = ""; - for (let i = 0; i < padding_size; i++) { + for (let i = 0; i < paddingSize; i++) { padding += letters.charAt(Math.floor(Math.random() * letters.length)); } - return encrypt(key, padding+plaintext+'---'+signature); + return encrypt(key, padding+plaintext+"---"+signature); } -export function decrypt_pad(key, ciphertext, padding_size) { - init_tiles(); - check_key(key); - return decrypt(key, ciphertext).slice(padding_size); -} \ No newline at end of file +/** + * + */ +export function decryptPad(key, ciphertext, paddingSize) { + initTiles(); + checkKey(key); + return decrypt(key, ciphertext).slice(paddingSize); +} diff --git a/src/core/operations/LS47Decrypt.mjs b/src/core/operations/LS47Decrypt.mjs index ffda8f93..a5a92ebf 100644 --- a/src/core/operations/LS47Decrypt.mjs +++ b/src/core/operations/LS47Decrypt.mjs @@ -5,7 +5,7 @@ */ import Operation from "../Operation.mjs"; -import * as LS47 from "../lib/LS47.mjs" +import * as LS47 from "../lib/LS47.mjs"; /** * LS47 Decrypt operation @@ -45,12 +45,12 @@ class LS47Decrypt extends Operation { */ run(input, args) { - this.padding_size = parseInt(args[1], 10); + this.paddingSize = parseInt(args[1], 10); - LS47.init_tiles(); - - let key = LS47.derive_key(args[0]); - return LS47.decrypt_pad(key, input, this.padding_size); + LS47.initTiles(); + + const key = LS47.deriveKey(args[0]); + return LS47.decryptPad(key, input, this.paddingSize); } } diff --git a/src/core/operations/LS47Encrypt.mjs b/src/core/operations/LS47Encrypt.mjs index bf3b0306..f82baaab 100644 --- a/src/core/operations/LS47Encrypt.mjs +++ b/src/core/operations/LS47Encrypt.mjs @@ -5,7 +5,7 @@ */ import Operation from "../Operation.mjs"; -import * as LS47 from "../lib/LS47.mjs" +import * as LS47 from "../lib/LS47.mjs"; /** * LS47 Encrypt operation @@ -49,13 +49,13 @@ class LS47Encrypt extends Operation { * @returns {string} */ run(input, args) { - - this.padding_size = parseInt(args[1], 10); - LS47.init_tiles(); - - let key = LS47.derive_key(args[0]); - return LS47.encrypt_pad(key, input, args[2], this.padding_size); + this.paddingSize = parseInt(args[1], 10); + + LS47.initTiles(); + + const key = LS47.deriveKey(args[0]); + return LS47.encryptPad(key, input, args[2], this.paddingSize); } } From 6fd929160d9eb5ee332af80c90e823513b0a86f1 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Tue, 28 Jan 2020 10:35:01 +0000 Subject: [PATCH 027/686] Comments and linting. --- src/core/lib/LS47.mjs | 57 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/core/lib/LS47.mjs b/src/core/lib/LS47.mjs index b028fc4f..6696aafc 100644 --- a/src/core/lib/LS47.mjs +++ b/src/core/lib/LS47.mjs @@ -10,7 +10,7 @@ const letters = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"; const tiles = []; /** - * + * Initialises the tiles with values and positions. */ export function initTiles() { for (let i = 0; i < 49; i++) @@ -18,7 +18,12 @@ export function initTiles() { } /** + * Rotates the key "down". * + * @param {string} key + * @param {number} col + * @param {number} n + * @returns {string} */ function rotateDown(key, col, n) { const lines = []; @@ -41,7 +46,12 @@ function rotateDown(key, col, n) { } /** + * Rotates the key "right". * + * @param {string} key + * @param {number} row + * @param {number} n + * @returns {string} */ function rotateRight(key, row, n) { const mid = key.slice(row * 7, (row + 1) * 7); @@ -50,7 +60,10 @@ function rotateRight(key, row, n) { } /** + * Finds the position of a letter in the tiles. * + * @param {string} letter + * @returns {string} */ function findIx(letter) { for (let i = 0; i < tiles.length; i++) @@ -60,7 +73,10 @@ function findIx(letter) { } /** + * Derives key from the input password. * + * @param {string} password + * @returns {string} */ export function deriveKey(password) { let i = 0; @@ -74,7 +90,9 @@ export function deriveKey(password) { } /** + * Checks the key is a valid key. * + * @param {string} key */ function checkKey(key) { if (key.length !== letters.length) @@ -92,7 +110,11 @@ function checkKey(key) { } /** + * Finds the position of a letter in they key. * + * @param {letter} key + * @param {string} letter + * @returns {object} */ function findPos (key, letter) { const index = key.indexOf(letter); @@ -102,21 +124,35 @@ function findPos (key, letter) { } /** + * Returns the character at the position on the tiles. * + * @param {string} key + * @param {object} coord + * @returns {string} */ function findAtPos(key, coord) { return key.charAt(coord[1] + (coord[0] * 7)); } /** + * Returns new position by adding two positions. * + * @param {object} a + * @param {object} b + * @returns {object} */ function addPos(a, b) { return [(a[0] + b[0]) % 7, (a[1] + b[1]) % 7]; } /** + * Returns new position by subtracting two positions. + * Note: We have to manually do the remainder division, since JS does not + * operate correctly on negative numbers (e.g. -3 % 4 = -3 when it should be 1). * + * @param {object} a + * @param {object} b + * @returns {object} */ function subPos(a, b) { const asub = a[0] - b[0]; @@ -125,7 +161,11 @@ function subPos(a, b) { } /** + * Encrypts the plaintext string. * + * @param {string} key + * @param {string} plaintext + * @returns {string} */ function encrypt(key, plaintext) { checkKey(key); @@ -146,7 +186,11 @@ function encrypt(key, plaintext) { } /** + * Decrypts the ciphertext string. * + * @param {string} key + * @param {string} ciphertext + * @returns {string} */ function decrypt(key, ciphertext) { checkKey(key); @@ -167,7 +211,13 @@ function decrypt(key, ciphertext) { } /** + * Adds padding to the input. * + * @param {string} key + * @param {string} plaintext + * @param {string} signature + * @param {number} paddingSize + * @returns {string} */ export function encryptPad(key, plaintext, signature, paddingSize) { initTiles(); @@ -180,7 +230,12 @@ export function encryptPad(key, plaintext, signature, paddingSize) { } /** + * Removes padding from the ouput. * + * @param {string} key + * @param {string} ciphertext + * @param {number} paddingSize + * @returns {string} */ export function decryptPad(key, ciphertext, paddingSize) { initTiles(); From e71794d362cf8112fc940a2ae6177c84ffce3bb5 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Fri, 14 Feb 2020 12:28:12 +0000 Subject: [PATCH 028/686] Tests added for LS47 --- src/core/operations/LS47Decrypt.mjs | 4 +-- src/core/operations/LS47Encrypt.mjs | 4 +-- tests/operations/index.mjs | 1 + tests/operations/tests/LS47.mjs | 45 +++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 tests/operations/tests/LS47.mjs diff --git a/src/core/operations/LS47Decrypt.mjs b/src/core/operations/LS47Decrypt.mjs index a5a92ebf..cb92cd27 100644 --- a/src/core/operations/LS47Decrypt.mjs +++ b/src/core/operations/LS47Decrypt.mjs @@ -20,8 +20,8 @@ class LS47Decrypt extends Operation { this.name = "LS47 Decrypt"; this.module = "Crypto"; - this.description = ""; - this.infoURL = ""; + this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; + this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master"; this.inputType = "string"; this.outputType = "string"; this.args = [ diff --git a/src/core/operations/LS47Encrypt.mjs b/src/core/operations/LS47Encrypt.mjs index f82baaab..51283844 100644 --- a/src/core/operations/LS47Encrypt.mjs +++ b/src/core/operations/LS47Encrypt.mjs @@ -20,8 +20,8 @@ class LS47Encrypt extends Operation { this.name = "LS47 Encrypt"; this.module = "Crypto"; - this.description = ""; - this.infoURL = ""; + this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; + this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master"; this.inputType = "string"; this.outputType = "string"; this.args = [ diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index bf440414..b3731727 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -96,6 +96,7 @@ import "./tests/DefangIP.mjs"; import "./tests/ParseUDP.mjs"; import "./tests/AvroToJSON.mjs"; import "./tests/Lorenz.mjs"; +import "./tests/LS47.mjs"; // Cannot test operations that use the File type yet diff --git a/tests/operations/tests/LS47.mjs b/tests/operations/tests/LS47.mjs new file mode 100644 index 00000000..40d876ee --- /dev/null +++ b/tests/operations/tests/LS47.mjs @@ -0,0 +1,45 @@ +/** + * Cartesian Product tests. + * + * @author n1073645 [n1073645@gmail.com] + * + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "LS47 Encrypt", + input: "thequickbrownfoxjumped", + expectedOutput: "(,t74ci78cp/8trx*yesu:alp1wqy", + recipeConfig: [ + { + op: "LS47 Encrypt", + args: ["helloworld", 0, "test"], + }, + ], + }, + { + name: "LS47 Decrypt", + input: "(,t74ci78cp/8trx*yesu:alp1wqy", + expectedOutput: "thequickbrownfoxjumped---test", + recipeConfig: [ + { + op: "LS47 Decrypt", + args: ["helloworld", 0], + }, + ], + }, + { + name: "LS47 Encrypt", + input: "thequickbrownfoxjumped", + expectedOutput: "Letter H is not included in LS47", + recipeConfig: [ + { + op: "LS47 Encrypt", + args: ["Helloworld", 0, "test"], + }, + ], + } +]); From e91e993fb5e7ec99db8fcb179fbd18a3f53b97bc Mon Sep 17 00:00:00 2001 From: n1073645 <57447333+n1073645@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:43:30 +0000 Subject: [PATCH 029/686] Update LS47.mjs --- tests/operations/tests/LS47.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/operations/tests/LS47.mjs b/tests/operations/tests/LS47.mjs index 40d876ee..ce613923 100644 --- a/tests/operations/tests/LS47.mjs +++ b/tests/operations/tests/LS47.mjs @@ -1,5 +1,5 @@ /** - * Cartesian Product tests. + * LS47 tests. * * @author n1073645 [n1073645@gmail.com] * From 0182cdda69f7c877746084a75600d87b2cb34e19 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Sat, 16 May 2020 00:42:02 +0200 Subject: [PATCH 030/686] Base85: Fix alphabetName --- src/core/lib/Base85.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/lib/Base85.mjs b/src/core/lib/Base85.mjs index 8da729e2..e5778132 100644 --- a/src/core/lib/Base85.mjs +++ b/src/core/lib/Base85.mjs @@ -1,3 +1,5 @@ +import Utils from "../Utils.mjs"; + /** * Base85 resources. * @@ -32,13 +34,12 @@ export const ALPHABET_OPTIONS = [ * @returns {string} */ export function alphabetName(alphabet) { - alphabet = alphabet.replace("'", "'"); - alphabet = alphabet.replace("\"", """); - alphabet = alphabet.replace("\\", "\"); + alphabet = escape(alphabet); let name; ALPHABET_OPTIONS.forEach(function(a) { - if (escape(alphabet) === escape(a.value)) name = a.name; + const expanded = Utils.expandAlphRange(a.value).join(""); + if (alphabet === escape(expanded)) name = a.name; }); return name; From 103ecff6a7465b7a46a8f452885ec99d0e45ea26 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Sat, 16 May 2020 00:42:31 +0200 Subject: [PATCH 031/686] Base85: Ignore whitespace --- src/core/operations/FromBase85.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index c874d5dc..c0d0328e 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -52,6 +52,8 @@ class FromBase85 extends Operation { if (input.length === 0) return []; + input = input.replace(/\s+/g, ""); + const matches = input.match(/<~(.+?)~>/); if (matches !== null) input = matches[1]; From 15dd9d4c93fa5bcfb1341ad8dccdb5671ae08d22 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Sat, 16 May 2020 00:42:50 +0200 Subject: [PATCH 032/686] Add magic checks for base85 --- src/core/operations/FromBase85.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index c0d0328e..42f37a1c 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -33,6 +33,23 @@ class FromBase85 extends Operation { value: ALPHABET_OPTIONS }, ]; + this.checks = [ + { + pattern: "^\\s*(?:<~)?(?:(?:\\s*[!-u]){5}|\\s*z)+[!-u\\s]*(?:~>)?\\s*$", + flags: "i", + args: ["!-u"] + }, + { + pattern: "^(?:\\s*[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#])+\\s*$", + flags: "i", + args: ["0-9a-zA-Z.\\-:+=^!/*?&<>()[]{}@%$#"] + }, + { + pattern: "^(?:\\s*[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~])+\\s*$", + flags: "i", + args: ["0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~"] + }, + ]; } /** From eab1be0e2c58c3d69f8b2c477e4102f601b611c7 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Wed, 20 May 2020 00:23:50 +0200 Subject: [PATCH 033/686] Magic base85: Remove 'i' flag --- src/core/operations/FromBase85.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index 42f37a1c..22033f99 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -36,17 +36,14 @@ class FromBase85 extends Operation { this.checks = [ { pattern: "^\\s*(?:<~)?(?:(?:\\s*[!-u]){5}|\\s*z)+[!-u\\s]*(?:~>)?\\s*$", - flags: "i", args: ["!-u"] }, { pattern: "^(?:\\s*[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#])+\\s*$", - flags: "i", args: ["0-9a-zA-Z.\\-:+=^!/*?&<>()[]{}@%$#"] }, { pattern: "^(?:\\s*[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~])+\\s*$", - flags: "i", args: ["0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~"] }, ]; From 1294d764e258bb6caa739b6111bb6d79a61d394f Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Fri, 22 May 2020 03:30:15 +0200 Subject: [PATCH 034/686] Base85: Only remove start and end markers with standard/ascii85 encoding --- src/core/operations/FromBase85.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index 22033f99..09ded171 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -68,8 +68,10 @@ class FromBase85 extends Operation { input = input.replace(/\s+/g, ""); - const matches = input.match(/<~(.+?)~>/); - if (matches !== null) input = matches[1]; + if (encoding === "Standard") { + const matches = input.match(/<~(.+?)~>/); + if (matches !== null) input = matches[1]; + } let i = 0; let block, blockBytes; From ee408f7add6d633b9c42a6677f5bfa75055e9ca6 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Fri, 22 May 2020 03:30:57 +0200 Subject: [PATCH 035/686] Base85: Update magic regexes to require 20 non-whitespace base85 chars --- src/core/operations/FromBase85.mjs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index 09ded171..9d73baa1 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -35,16 +35,31 @@ class FromBase85 extends Operation { ]; this.checks = [ { - pattern: "^\\s*(?:<~)?(?:(?:\\s*[!-u]){5}|\\s*z)+[!-u\\s]*(?:~>)?\\s*$", - args: ["!-u"] + pattern: + "^\\s*(?:<~)?" + // Optional whitespace and starting marker + "[\\s!-uz]*" + // Any amount of base85 characters and whitespace + "[!-uz]{20}" + // At least 20 continoues base85 characters without whitespace + "[\\s!-uz]*" + // Any amount of base85 characters and whitespace + "(?:~>)?\\s*$", // Optional ending marker and whitespace + args: ["!-u"], }, { - pattern: "^(?:\\s*[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#])+\\s*$", - args: ["0-9a-zA-Z.\\-:+=^!/*?&<>()[]{}@%$#"] + pattern: + "^" + + "[\\s0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]*" + + "[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]{20}" + // At least 20 continoues base85 characters without whitespace + "[\\s0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]*" + + "$", + args: ["0-9a-zA-Z.\\-:+=^!/*?&<>()[]{}@%$#"], }, { - pattern: "^(?:\\s*[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~])+\\s*$", - args: ["0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~"] + pattern: + "^" + + "[\\s0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]*" + + "[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]{20}" + // At least 20 continoues base85 characters without whitespace + "[\\s0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]*" + + "$", + args: ["0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~"], }, ]; } From f007c093eb3820161ab801a83e94a9da45d4f961 Mon Sep 17 00:00:00 2001 From: hettysymes <59455170+hettysymes@users.noreply.github.com> Date: Sun, 12 Jan 2020 15:06:41 +0000 Subject: [PATCH 036/686] Emulation of the WW2 SIGABA machine I have created an emulation of the SIGABA machine and have tested it against some test data from a Master's thesis by Miao Ai: https://scholarworks.sjsu.edu/cgi/viewcontent.cgi?article=1237&context=etd_projects --- src/core/lib/SIGABA.mjs | 501 +++++++++++++++++++++++++++++++++ src/core/operations/SIGABA.mjs | 293 +++++++++++++++++++ 2 files changed, 794 insertions(+) create mode 100644 src/core/lib/SIGABA.mjs create mode 100644 src/core/operations/SIGABA.mjs diff --git a/src/core/lib/SIGABA.mjs b/src/core/lib/SIGABA.mjs new file mode 100644 index 00000000..c35eb3a5 --- /dev/null +++ b/src/core/lib/SIGABA.mjs @@ -0,0 +1,501 @@ +/** +Emulation of the SIGABA machine + +@author hettysymes +*/ + +/** +A set of randomised example SIGABA cipher/control rotors (these rotors are interchangeable). Cipher and control rotors can be referred to as C and R rotors respectively. +*/ + +export const CR_ROTORS = [ + {name: "Example 1", value: "SRGWANHPJZFXVIDQCEUKBYOLMT"}, + {name: "Example 2", value: "THQEFSAZVKJYULBODCPXNIMWRG"}, + {name: "Example 3", value: "XDTUYLEVFNQZBPOGIRCSMHWKAJ"}, + {name: "Example 4", value: "LOHDMCWUPSTNGVXYFJREQIKBZA"}, + {name: "Example 5", value: "ERXWNZQIJYLVOFUMSGHTCKPBDA"}, + {name: "Example 6", value: "FQECYHJIOUMDZVPSLKRTGWXBAN"}, + {name: "Example 7", value: "TBYIUMKZDJSOPEWXVANHLCFQGR"}, + {name: "Example 8", value: "QZUPDTFNYIAOMLEBWJXCGHKRSV"}, + {name: "Example 9", value: "CZWNHEMPOVXLKRSIDGJFYBTQAU"}, + {name: "Example 10", value: "ENPXJVKYQBFZTICAGMOHWRLDUS"} +]; + +/** +A set of randomised example SIGABA index rotors (may be referred to as I rotors). +*/ + +export const I_ROTORS = [ + {name: "Example 1", value: "6201348957"}, + {name: "Example 2", value: "6147253089"}, + {name: "Example 3", value: "8239647510"}, + {name: "Example 4", value: "7194835260"}, + {name: "Example 5", value: "4873205916"} +]; + +export const NUMBERS = "0123456789".split(""); + +/** +Converts a letter to uppercase (if it already isn't) + +@param {char} letter - letter to convert to upper case +@returns {char} +*/ +export function convToUpperCase(letter){ + const charCode = letter.charCodeAt(); + if (97<=charCode && charCode<=122){ + return String.fromCharCode(charCode-32); + } + return letter; +} + +/** +The SIGABA machine consisting of the 3 rotor banks: cipher, control and index banks. +*/ +export class SigabaMachine{ + /** + SigabaMachine constructor + + @param {Object[]} cipherRotors - list of CRRotors + @param {Object[]} controlRotors - list of CRRotors + @param {object[]} indexRotors - list of IRotors + */ + constructor(cipherRotors, controlRotors, indexRotors){ + this.cipherBank = new CipherBank(cipherRotors); + this.controlBank = new ControlBank(controlRotors); + this.indexBank = new IndexBank(indexRotors); + } + + /** + Steps all the correct rotors in the machine. + */ + step(){ + const controlOut = this.controlBank.goThroughControl(); + const indexOut = this.indexBank.goThroughIndex(controlOut); + this.cipherBank.step(indexOut); + } + + /** + Encrypts a letter. A space is converted to a "Z" before encryption, and a "Z" is converted to an "X". This allows spaces to be encrypted. + + @param {char} letter - letter to encrypt + @returns {char} + */ + encryptLetter(letter){ + letter = convToUpperCase(letter); + if (letter == " "){ + letter = "Z"; + } + else if (letter == "Z") { + letter = "X"; + } + const encryptedLetter = this.cipherBank.encrypt(letter); + this.step(); + return encryptedLetter; + } + + /** + Decrypts a letter. A letter decrypted as a "Z" is converted to a space before it is output, since spaces are converted to "Z"s before encryption. + + @param {char} letter - letter to decrypt + @returns {char} + */ + decryptLetter(letter){ + letter = convToUpperCase(letter); + let decryptedLetter = this.cipherBank.decrypt(letter); + if (decryptedLetter == "Z"){ + decryptedLetter = " "; + } + this.step(); + return decryptedLetter; + } + + /** + Encrypts a message of one or more letters + + @param {string} msg - message to encrypt + @returns {string} + */ + encrypt(msg){ + let ciphertext = ""; + for (const letter of msg){ + ciphertext = ciphertext.concat(this.encryptLetter(letter)); + } + return ciphertext; + } + + /** + Decrypts a message of one or more letters + + @param {string} msg - message to decrypt + @returns {string} + */ + decrypt(msg){ + let plaintext = ""; + for (const letter of msg){ + plaintext = plaintext.concat(this.decryptLetter(letter)); + } + return plaintext; + } + +} + +/** +The cipher rotor bank consists of 5 cipher rotors in either a forward or reversed orientation. +*/ +export class CipherBank{ + /** + CipherBank constructor + + @param {Object[]} rotors - list of CRRotors + */ + constructor(rotors){ + this.rotors = rotors; + } + + /** + Encrypts a letter through the cipher rotors (signal goes from left-to-right) + + @param {char} inputPos - the input position of the signal (letter to be encrypted) + @returns {char} + */ + encrypt(inputPos){ + for (let rotor of this.rotors){ + inputPos = rotor.crypt(inputPos, "leftToRight"); + } + return inputPos; + } + + /** + Decrypts a letter through the cipher rotors (signal goes from right-to-left) + + @param {char} inputPos - the input position of the signal (letter to be decrypted) + @returns {char} + */ + decrypt(inputPos){ + const revOrderedRotors = [...this.rotors].reverse(); + for (let rotor of revOrderedRotors){ + inputPos = rotor.crypt(inputPos, "rightToLeft"); + } + return inputPos; + } + + /** + Step the cipher rotors forward according to the inputs from the index rotors + + @param {number[]} indexInputs - the inputs from the index rotors + */ + step(indexInputs){ + const logicDict = {0: [0,9], 1:[7,8], 2:[5,6], 3:[3,4], 4:[1,2]}; + let rotorsToMove = []; + for (const key in logicDict){ + const item = logicDict[key]; + for (const i of indexInputs){ + if (item.includes(i)){ + rotorsToMove.push(this.rotors[key]); + break; + } + } + } + for (let rotor of rotorsToMove){ + rotor.step(); + } + } + +} + +/** +The control rotor bank consists of 5 control rotors in either a forward or reversed orientation. Signals to the control rotor bank always go from right-to-left. +*/ +export class ControlBank{ + /** + ControlBank constructor. The rotors have been reversed as signals go from right-to-left through the control rotors. + + @param {Object[]} rotors - list of CRRotors + */ + constructor(rotors){ + this.rotors = [...rotors].reverse(); + this.numberOfMoves = 1; + } + + /** + Encrypts a letter. + + @param {char} inputPos - the input position of the signal + @returns {char} + */ + crypt(inputPos){ + for (let rotor of this.rotors){ + inputPos = rotor.crypt(inputPos, "rightToLeft"); + } + return inputPos; + } + + /** + Gets the outputs of the control rotors. The inputs to the control rotors are always "F", "G", "H" and "I". + + @returns {number[]} + */ + getOutputs(){ + const outputs = [this.crypt("F"), this.crypt("G"), this.crypt("H"), this.crypt("I")]; + const logicDict = {1:"B", 2:"C", 3:"DE", 4:"FGH", 5:"IJK", 6:"LMNO", 7:"PQRST", 8:"UVWXYZ", 9:"A"}; + let numberOutputs = []; + for (let key in logicDict){ + const item = logicDict[key]; + for (let output of outputs){ + if (item.includes(output)){ + numberOutputs.push(key); + break; + } + } + } + return numberOutputs; + } + + /** + Steps the control rotors. Only 3 of the control rotors step: one after every encryption, one after every 26, and one after every 26 squared. + */ + step(){ + const MRotor = this.rotors[1], FRotor = this.rotors[2], SRotor = this.rotors[3]; + this.numberOfMoves ++; + FRotor.step(); + if (this.numberOfMoves%26 == 0){ + MRotor.step(); + } + if (this.numberOfMoves%(26*26) == 0){ + SRotor.step(); + } + } + + /** + The goThroughControl function combines getting the outputs from the control rotor bank and then stepping them. + + @returns {number[]} + */ + goThroughControl(){ + const outputs = this.getOutputs(); + this.step(); + return outputs; + } + +} + +/** +The index rotor bank consists of 5 index rotors all placed in the forwards orientation. +*/ +export class IndexBank{ + /** + IndexBank constructor + + @param {Object[]} rotors - list of IRotors + */ + constructor(rotors){ + this.rotors = rotors; + } + + /** + Encrypts a number. + + @param {number} inputPos - the input position of the signal + @returns {number} + */ + crypt(inputPos){ + for (let rotor of this.rotors){ + inputPos = rotor.crypt(inputPos); + } + return inputPos; + } + + /** + The goThroughIndex function takes the inputs from the control rotor bank and returns the list of outputs after encryption through the index rotors. + + @param {number[]} - inputs from the control rotors + @returns {number[]} + */ + goThroughIndex(controlInputs){ + let outputs = []; + for (const inp of controlInputs){ + outputs.push(this.crypt(inp)); + } + return outputs; + } + +} + +/** +Rotor class +*/ +export class Rotor{ + /** + Rotor constructor + + @param {number[]} wireSetting - the wirings within the rotor: mapping from left-to-right, the index of the number in the list maps onto the number at that index + @param {bool} rev - true if the rotor is reversed, false if it isn't + @param {number} key - the starting position or state of the rotor + */ + constructor(wireSetting, key, rev){ + this.state = key; + this.numMapping = this.getNumMapping(wireSetting, rev); + this.posMapping = this.getPosMapping(rev); + } + + /** + Get the number mapping from the wireSetting (only different from wireSetting if rotor is reversed) + + @param {number[]} wireSetting - the wirings within the rotors + @param {bool} rev - true if reversed, false if not + @returns {number[]} + */ + getNumMapping(wireSetting, rev){ + if (rev==false){ + return wireSetting; + } + else { + const length = wireSetting.length; + let tempMapping = new Array(length); + for (let i=0; ithis.state-length; i--){ + let res = i%length; + if (res<0){ + res += length; + } + posMapping.push(res); + } + } + return posMapping; + } + + /** + Encrypt/decrypt data. This process is identical to the rotors of cipher machines such as Enigma or Typex. + + @param {number} inputPos - the input position of the signal (the data to encrypt/decrypt) + @param {string} direction - one of "leftToRight" and "rightToLeft", states the direction in which the signal passes through the rotor + @returns {number} + */ + cryptNum(inputPos, direction){ + const inpNum = this.posMapping[inputPos]; + var outNum; + if (direction == "leftToRight"){ + outNum = this.numMapping[inpNum]; + } + else if (direction == "rightToLeft") { + outNum = this.numMapping.indexOf(inpNum); + } + const outPos = this.posMapping.indexOf(outNum); + return outPos; + } + + /** + Steps the rotor. The number at position 0 will be moved to position 1 etc. + */ + step(){ + const lastNum = this.posMapping.pop(); + this.posMapping.splice(0, 0, lastNum); + this.state = this.posMapping[0]; + } + +} + +/** +A CRRotor is a cipher (C) or control (R) rotor. These rotors are identical and interchangeable. A C or R rotor consists of 26 contacts, one for each letter, and may be put into either a forwards of reversed orientation. +*/ +export class CRRotor extends Rotor{ + + /** + CRRotor constructor + + @param {string} wireSetting - the rotor wirings (string of letters) + @param {char} key - initial state of rotor + @param {bool} rev - true if reversed, false if not + */ + constructor(wireSetting, key, rev=false){ + wireSetting = wireSetting.split("").map(CRRotor.letterToNum); + super(wireSetting, CRRotor.letterToNum(key), rev); + } + + /** + Static function which converts a letter into its number i.e. its offset from the letter "A" + + @param {char} letter - letter to convert to number + @returns {number} + */ + static letterToNum(letter){ + return letter.charCodeAt()-65; + } + + /** + Static function which converts a number (a letter's offset from "A") into its letter + + @param {number} num - number to convert to letter + @returns {char} + */ + static numToLetter(num){ + return String.fromCharCode(num+65); + } + + /** + Encrypts/decrypts a letter. + + @param {char} inputPos - the input position of the signal ("A" refers to position 0 etc.) + @param {string} direction - one of "leftToRight" and "rightToLeft" + @returns {char} + */ + crypt(inputPos, direction){ + inputPos = CRRotor.letterToNum(inputPos); + const outPos = this.cryptNum(inputPos, direction); + return CRRotor.numToLetter(outPos); + } + +} + +/** +An IRotor is an index rotor, which consists of 10 contacts each numbered from 0 to 9. Unlike C and R rotors, they cannot be put in the reversed orientation. The index rotors do not step at any point during encryption or decryption. +*/ +export class IRotor extends Rotor{ + /** + IRotor constructor + + @param {string} wireSetting - the rotor wirings (string of numbers) + @param {char} key - initial state of rotor + */ + constructor(wireSetting, key){ + wireSetting = wireSetting.split("").map(Number); + super(wireSetting, Number(key), false); + } + + /** + Encrypts a number + + @param {number} inputPos - the input position of the signal + @returns {number} + */ + crypt(inputPos){ + return this.cryptNum(inputPos, "leftToRight"); + } + +} diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs new file mode 100644 index 00000000..78f05530 --- /dev/null +++ b/src/core/operations/SIGABA.mjs @@ -0,0 +1,293 @@ +/** +Emulation of the SIGABA machine. + +@author hettysymes +*/ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import {LETTERS} from "../lib/Enigma.mjs"; +import {NUMBERS, CR_ROTORS, I_ROTORS, SigabaMachine, CRRotor, IRotor} from "../lib/SIGABA.mjs"; + +/** +Sigaba operation +*/ +class Sigaba extends Operation{ +/** +Sigaba constructor +*/ +constructor(){ +super(); + +this.name = "SIGABA"; +this.module = "SIGABA"; + this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. The idea behind its design was to truly randomise the motion of the rotors. In comparison, Enigma, which rotates its rotors once every key pressed, has much less randomised rotor movements. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; + this.infoURL = "https://en.wikipedia.org/wiki/SIGABA"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "1st (left-hand) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "1st cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "1st cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "2nd cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "2nd cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "3rd (middle) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "3rd cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "4th cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "4th cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "4th cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "5th (right-hand) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "5th cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "5th cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "1st (left-hand) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "1st control rotor reversed", + type: "boolean", + value: false + }, + { + name: "1st control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "2nd control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd control rotor reversed", + type: "boolean", + value: false + }, + { + name: "2nd control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "3rd (middle) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd control rotor reversed", + type: "boolean", + value: false + }, + { + name: "3rd control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "4th control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "4th control rotor reversed", + type: "boolean", + value: false + }, + { + name: "4th control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "5th (right-hand) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "5th control rotor reversed", + type: "boolean", + value: false + }, + { + name: "5th control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "1st (left-hand) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "1st index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "2nd index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "3rd (middle) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "4th index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "4th index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "5th (right-hand) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "5th index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "SIGABA mode", + type: "option", + value: ["Encrypt", "Decrypt"] + } + ]; + } + + /** + @param {string} rotor - rotor wirings + @returns {string} + */ + + parseRotorStr(rotor){ + if (rotor === ""){ + throw new OperationError(`All rotor wirings must be provided.`); + } + return rotor; + } + + run(input, args){ + const sigabaSwitch = args[40]; + const cipherRotors = []; + const controlRotors = []; + const indexRotors = []; + for (let i=0; i<5; i++){ + const rotorWiring = this.parseRotorStr(args[i*3]); + cipherRotors.push(new CRRotor(rotorWiring, args[i*3+2], args[i*3+1])); + } + for (let i=5; i<10; i++){ + const rotorWiring = this.parseRotorStr(args[i*3]); + controlRotors.push(new CRRotor(rotorWiring, args[i*3+2], args[i*3+1])); + } + for (let i=15; i<20; i++){ + const rotorWiring = this.parseRotorStr(args[i*2]); + indexRotors.push(new IRotor(rotorWiring, args[i*2+1])); + } + const sigaba = new SigabaMachine(cipherRotors, controlRotors, indexRotors); + var result; + if (sigabaSwitch === "Encrypt"){ + result = sigaba.encrypt(input); + } + else if (sigabaSwitch === "Decrypt") { + result = sigaba.decrypt(input); + } + return result; + } + +} +export default Sigaba; From 5d01b06877417120826826f823565c202a022b53 Mon Sep 17 00:00:00 2001 From: hettysymes <59455170+hettysymes@users.noreply.github.com> Date: Sun, 12 Jan 2020 15:37:07 +0000 Subject: [PATCH 037/686] Added copyright and clarified description --- src/core/lib/SIGABA.mjs | 2 ++ src/core/operations/SIGABA.mjs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/lib/SIGABA.mjs b/src/core/lib/SIGABA.mjs index c35eb3a5..b56b3e24 100644 --- a/src/core/lib/SIGABA.mjs +++ b/src/core/lib/SIGABA.mjs @@ -2,6 +2,8 @@ Emulation of the SIGABA machine @author hettysymes +@copyright hettysymes 2020 +@license Apache-2.0 */ /** diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs index 78f05530..2f42c501 100644 --- a/src/core/operations/SIGABA.mjs +++ b/src/core/operations/SIGABA.mjs @@ -2,6 +2,8 @@ Emulation of the SIGABA machine. @author hettysymes +@copyright hettysymes 2020 +@license Apache-2.0 */ import Operation from "../Operation.mjs"; @@ -21,7 +23,7 @@ super(); this.name = "SIGABA"; this.module = "SIGABA"; - this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. The idea behind its design was to truly randomise the motion of the rotors. In comparison, Enigma, which rotates its rotors once every key pressed, has much less randomised rotor movements. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; + this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex than other rotor machines of its time, such as Enigma. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; this.infoURL = "https://en.wikipedia.org/wiki/SIGABA"; this.inputType = "string"; this.outputType = "string"; From 938385c18b5b66c691fc08ed38949491107c75de Mon Sep 17 00:00:00 2001 From: hettysymes <59455170+hettysymes@users.noreply.github.com> Date: Sun, 12 Jan 2020 16:49:04 +0000 Subject: [PATCH 038/686] Fixed grunt lint errors --- src/core/lib/SIGABA.mjs | 156 +++++----- src/core/operations/SIGABA.mjs | 501 ++++++++++++++++----------------- 2 files changed, 322 insertions(+), 335 deletions(-) diff --git a/src/core/lib/SIGABA.mjs b/src/core/lib/SIGABA.mjs index b56b3e24..b69c7739 100644 --- a/src/core/lib/SIGABA.mjs +++ b/src/core/lib/SIGABA.mjs @@ -43,9 +43,9 @@ Converts a letter to uppercase (if it already isn't) @param {char} letter - letter to convert to upper case @returns {char} */ -export function convToUpperCase(letter){ +export function convToUpperCase(letter) { const charCode = letter.charCodeAt(); - if (97<=charCode && charCode<=122){ + if (97<=charCode && charCode<=122) { return String.fromCharCode(charCode-32); } return letter; @@ -54,7 +54,7 @@ export function convToUpperCase(letter){ /** The SIGABA machine consisting of the 3 rotor banks: cipher, control and index banks. */ -export class SigabaMachine{ +export class SigabaMachine { /** SigabaMachine constructor @@ -62,7 +62,7 @@ export class SigabaMachine{ @param {Object[]} controlRotors - list of CRRotors @param {object[]} indexRotors - list of IRotors */ - constructor(cipherRotors, controlRotors, indexRotors){ + constructor(cipherRotors, controlRotors, indexRotors) { this.cipherBank = new CipherBank(cipherRotors); this.controlBank = new ControlBank(controlRotors); this.indexBank = new IndexBank(indexRotors); @@ -71,7 +71,7 @@ export class SigabaMachine{ /** Steps all the correct rotors in the machine. */ - step(){ + step() { const controlOut = this.controlBank.goThroughControl(); const indexOut = this.indexBank.goThroughIndex(controlOut); this.cipherBank.step(indexOut); @@ -83,12 +83,11 @@ export class SigabaMachine{ @param {char} letter - letter to encrypt @returns {char} */ - encryptLetter(letter){ + encryptLetter(letter) { letter = convToUpperCase(letter); - if (letter == " "){ + if (letter === " ") { letter = "Z"; - } - else if (letter == "Z") { + } else if (letter === "Z") { letter = "X"; } const encryptedLetter = this.cipherBank.encrypt(letter); @@ -102,10 +101,10 @@ export class SigabaMachine{ @param {char} letter - letter to decrypt @returns {char} */ - decryptLetter(letter){ + decryptLetter(letter) { letter = convToUpperCase(letter); let decryptedLetter = this.cipherBank.decrypt(letter); - if (decryptedLetter == "Z"){ + if (decryptedLetter === "Z") { decryptedLetter = " "; } this.step(); @@ -118,9 +117,9 @@ export class SigabaMachine{ @param {string} msg - message to encrypt @returns {string} */ - encrypt(msg){ + encrypt(msg) { let ciphertext = ""; - for (const letter of msg){ + for (const letter of msg) { ciphertext = ciphertext.concat(this.encryptLetter(letter)); } return ciphertext; @@ -132,9 +131,9 @@ export class SigabaMachine{ @param {string} msg - message to decrypt @returns {string} */ - decrypt(msg){ + decrypt(msg) { let plaintext = ""; - for (const letter of msg){ + for (const letter of msg) { plaintext = plaintext.concat(this.decryptLetter(letter)); } return plaintext; @@ -145,13 +144,13 @@ export class SigabaMachine{ /** The cipher rotor bank consists of 5 cipher rotors in either a forward or reversed orientation. */ -export class CipherBank{ +export class CipherBank { /** CipherBank constructor @param {Object[]} rotors - list of CRRotors */ - constructor(rotors){ + constructor(rotors) { this.rotors = rotors; } @@ -161,8 +160,8 @@ export class CipherBank{ @param {char} inputPos - the input position of the signal (letter to be encrypted) @returns {char} */ - encrypt(inputPos){ - for (let rotor of this.rotors){ + encrypt(inputPos) { + for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos, "leftToRight"); } return inputPos; @@ -174,9 +173,9 @@ export class CipherBank{ @param {char} inputPos - the input position of the signal (letter to be decrypted) @returns {char} */ - decrypt(inputPos){ + decrypt(inputPos) { const revOrderedRotors = [...this.rotors].reverse(); - for (let rotor of revOrderedRotors){ + for (const rotor of revOrderedRotors) { inputPos = rotor.crypt(inputPos, "rightToLeft"); } return inputPos; @@ -187,19 +186,19 @@ export class CipherBank{ @param {number[]} indexInputs - the inputs from the index rotors */ - step(indexInputs){ - const logicDict = {0: [0,9], 1:[7,8], 2:[5,6], 3:[3,4], 4:[1,2]}; - let rotorsToMove = []; - for (const key in logicDict){ + step(indexInputs) { + const logicDict = {0: [0, 9], 1: [7, 8], 2: [5, 6], 3: [3, 4], 4: [1, 2]}; + const rotorsToMove = []; + for (const key in logicDict) { const item = logicDict[key]; - for (const i of indexInputs){ - if (item.includes(i)){ + for (const i of indexInputs) { + if (item.includes(i)) { rotorsToMove.push(this.rotors[key]); break; } } } - for (let rotor of rotorsToMove){ + for (const rotor of rotorsToMove) { rotor.step(); } } @@ -209,13 +208,13 @@ export class CipherBank{ /** The control rotor bank consists of 5 control rotors in either a forward or reversed orientation. Signals to the control rotor bank always go from right-to-left. */ -export class ControlBank{ +export class ControlBank { /** ControlBank constructor. The rotors have been reversed as signals go from right-to-left through the control rotors. @param {Object[]} rotors - list of CRRotors */ - constructor(rotors){ + constructor(rotors) { this.rotors = [...rotors].reverse(); this.numberOfMoves = 1; } @@ -226,8 +225,8 @@ export class ControlBank{ @param {char} inputPos - the input position of the signal @returns {char} */ - crypt(inputPos){ - for (let rotor of this.rotors){ + crypt(inputPos) { + for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos, "rightToLeft"); } return inputPos; @@ -238,14 +237,14 @@ export class ControlBank{ @returns {number[]} */ - getOutputs(){ + getOutputs() { const outputs = [this.crypt("F"), this.crypt("G"), this.crypt("H"), this.crypt("I")]; - const logicDict = {1:"B", 2:"C", 3:"DE", 4:"FGH", 5:"IJK", 6:"LMNO", 7:"PQRST", 8:"UVWXYZ", 9:"A"}; - let numberOutputs = []; - for (let key in logicDict){ + const logicDict = {1: "B", 2: "C", 3: "DE", 4: "FGH", 5: "IJK", 6: "LMNO", 7: "PQRST", 8: "UVWXYZ", 9: "A"}; + const numberOutputs = []; + for (const key in logicDict) { const item = logicDict[key]; - for (let output of outputs){ - if (item.includes(output)){ + for (const output of outputs) { + if (item.includes(output)) { numberOutputs.push(key); break; } @@ -257,14 +256,14 @@ export class ControlBank{ /** Steps the control rotors. Only 3 of the control rotors step: one after every encryption, one after every 26, and one after every 26 squared. */ - step(){ + step() { const MRotor = this.rotors[1], FRotor = this.rotors[2], SRotor = this.rotors[3]; this.numberOfMoves ++; FRotor.step(); - if (this.numberOfMoves%26 == 0){ + if (this.numberOfMoves%26 === 0) { MRotor.step(); } - if (this.numberOfMoves%(26*26) == 0){ + if (this.numberOfMoves%(26*26) === 0) { SRotor.step(); } } @@ -274,7 +273,7 @@ export class ControlBank{ @returns {number[]} */ - goThroughControl(){ + goThroughControl() { const outputs = this.getOutputs(); this.step(); return outputs; @@ -285,13 +284,13 @@ export class ControlBank{ /** The index rotor bank consists of 5 index rotors all placed in the forwards orientation. */ -export class IndexBank{ +export class IndexBank { /** IndexBank constructor @param {Object[]} rotors - list of IRotors */ - constructor(rotors){ + constructor(rotors) { this.rotors = rotors; } @@ -301,8 +300,8 @@ export class IndexBank{ @param {number} inputPos - the input position of the signal @returns {number} */ - crypt(inputPos){ - for (let rotor of this.rotors){ + crypt(inputPos) { + for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos); } return inputPos; @@ -314,9 +313,9 @@ export class IndexBank{ @param {number[]} - inputs from the control rotors @returns {number[]} */ - goThroughIndex(controlInputs){ - let outputs = []; - for (const inp of controlInputs){ + goThroughIndex(controlInputs) { + const outputs = []; + for (const inp of controlInputs) { outputs.push(this.crypt(inp)); } return outputs; @@ -327,7 +326,7 @@ export class IndexBank{ /** Rotor class */ -export class Rotor{ +export class Rotor { /** Rotor constructor @@ -335,7 +334,7 @@ export class Rotor{ @param {bool} rev - true if the rotor is reversed, false if it isn't @param {number} key - the starting position or state of the rotor */ - constructor(wireSetting, key, rev){ + constructor(wireSetting, key, rev) { this.state = key; this.numMapping = this.getNumMapping(wireSetting, rev); this.posMapping = this.getPosMapping(rev); @@ -348,14 +347,13 @@ export class Rotor{ @param {bool} rev - true if reversed, false if not @returns {number[]} */ - getNumMapping(wireSetting, rev){ - if (rev==false){ + getNumMapping(wireSetting, rev) { + if (rev===false) { return wireSetting; - } - else { + } else { const length = wireSetting.length; - let tempMapping = new Array(length); - for (let i=0; ithis.state-length; i--){ + } else { + for (let i = this.state; i > this.state-length; i--) { let res = i%length; - if (res<0){ + if (res<0) { res += length; } posMapping.push(res); @@ -399,13 +396,12 @@ export class Rotor{ @param {string} direction - one of "leftToRight" and "rightToLeft", states the direction in which the signal passes through the rotor @returns {number} */ - cryptNum(inputPos, direction){ + cryptNum(inputPos, direction) { const inpNum = this.posMapping[inputPos]; - var outNum; - if (direction == "leftToRight"){ + let outNum; + if (direction === "leftToRight") { outNum = this.numMapping[inpNum]; - } - else if (direction == "rightToLeft") { + } else if (direction === "rightToLeft") { outNum = this.numMapping.indexOf(inpNum); } const outPos = this.posMapping.indexOf(outNum); @@ -415,7 +411,7 @@ export class Rotor{ /** Steps the rotor. The number at position 0 will be moved to position 1 etc. */ - step(){ + step() { const lastNum = this.posMapping.pop(); this.posMapping.splice(0, 0, lastNum); this.state = this.posMapping[0]; @@ -426,7 +422,7 @@ export class Rotor{ /** A CRRotor is a cipher (C) or control (R) rotor. These rotors are identical and interchangeable. A C or R rotor consists of 26 contacts, one for each letter, and may be put into either a forwards of reversed orientation. */ -export class CRRotor extends Rotor{ +export class CRRotor extends Rotor { /** CRRotor constructor @@ -435,7 +431,7 @@ export class CRRotor extends Rotor{ @param {char} key - initial state of rotor @param {bool} rev - true if reversed, false if not */ - constructor(wireSetting, key, rev=false){ + constructor(wireSetting, key, rev=false) { wireSetting = wireSetting.split("").map(CRRotor.letterToNum); super(wireSetting, CRRotor.letterToNum(key), rev); } @@ -446,7 +442,7 @@ export class CRRotor extends Rotor{ @param {char} letter - letter to convert to number @returns {number} */ - static letterToNum(letter){ + static letterToNum(letter) { return letter.charCodeAt()-65; } @@ -456,7 +452,7 @@ export class CRRotor extends Rotor{ @param {number} num - number to convert to letter @returns {char} */ - static numToLetter(num){ + static numToLetter(num) { return String.fromCharCode(num+65); } @@ -467,7 +463,7 @@ export class CRRotor extends Rotor{ @param {string} direction - one of "leftToRight" and "rightToLeft" @returns {char} */ - crypt(inputPos, direction){ + crypt(inputPos, direction) { inputPos = CRRotor.letterToNum(inputPos); const outPos = this.cryptNum(inputPos, direction); return CRRotor.numToLetter(outPos); @@ -478,14 +474,14 @@ export class CRRotor extends Rotor{ /** An IRotor is an index rotor, which consists of 10 contacts each numbered from 0 to 9. Unlike C and R rotors, they cannot be put in the reversed orientation. The index rotors do not step at any point during encryption or decryption. */ -export class IRotor extends Rotor{ +export class IRotor extends Rotor { /** IRotor constructor @param {string} wireSetting - the rotor wirings (string of numbers) @param {char} key - initial state of rotor */ - constructor(wireSetting, key){ + constructor(wireSetting, key) { wireSetting = wireSetting.split("").map(Number); super(wireSetting, Number(key), false); } @@ -496,7 +492,7 @@ export class IRotor extends Rotor{ @param {number} inputPos - the input position of the signal @returns {number} */ - crypt(inputPos){ + crypt(inputPos) { return this.cryptNum(inputPos, "leftToRight"); } diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs index 2f42c501..d82ee09a 100644 --- a/src/core/operations/SIGABA.mjs +++ b/src/core/operations/SIGABA.mjs @@ -7,285 +7,276 @@ Emulation of the SIGABA machine. */ import Operation from "../Operation.mjs"; -import OperationError from "../errors/OperationError.mjs"; import {LETTERS} from "../lib/Enigma.mjs"; import {NUMBERS, CR_ROTORS, I_ROTORS, SigabaMachine, CRRotor, IRotor} from "../lib/SIGABA.mjs"; /** Sigaba operation */ -class Sigaba extends Operation{ -/** -Sigaba constructor -*/ -constructor(){ -super(); +class Sigaba extends Operation { + /** + Sigaba constructor + */ + constructor() { + super(); -this.name = "SIGABA"; -this.module = "SIGABA"; - this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex than other rotor machines of its time, such as Enigma. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; - this.infoURL = "https://en.wikipedia.org/wiki/SIGABA"; - this.inputType = "string"; - this.outputType = "string"; - this.args = [ - { - name: "1st (left-hand) cipher rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "1st cipher rotor reversed", - type: "boolean", - value: false - }, - { - name: "1st cipher rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "2nd cipher rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "2nd cipher rotor reversed", - type: "boolean", - value: false - }, - { - name: "2nd cipher rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "3rd (middle) cipher rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "3rd cipher rotor reversed", - type: "boolean", - value: false - }, - { - name: "3rd cipher rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "4th cipher rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "4th cipher rotor reversed", - type: "boolean", - value: false - }, - { - name: "4th cipher rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "5th (right-hand) cipher rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "5th cipher rotor reversed", - type: "boolean", - value: false - }, - { - name: "5th cipher rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "1st (left-hand) control rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "1st control rotor reversed", - type: "boolean", - value: false - }, - { - name: "1st control rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "2nd control rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "2nd control rotor reversed", - type: "boolean", - value: false - }, - { - name: "2nd control rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "3rd (middle) control rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "3rd control rotor reversed", - type: "boolean", - value: false - }, - { - name: "3rd control rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "4th control rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "4th control rotor reversed", - type: "boolean", - value: false - }, - { - name: "4th control rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "5th (right-hand) control rotor", - type: "editableOption", - value: CR_ROTORS, - defaultIndex: 0 - }, - { - name: "5th control rotor reversed", - type: "boolean", - value: false - }, - { - name: "5th control rotor intial value", - type: "option", - value: LETTERS - }, - { - name: "1st (left-hand) index rotor", - type: "editableOption", - value: I_ROTORS, - defaultIndex: 0 - }, - { - name: "1st index rotor intial value", - type: "option", - value: NUMBERS - }, - { - name: "2nd index rotor", - type: "editableOption", - value: I_ROTORS, - defaultIndex: 0 - }, - { - name: "2nd index rotor intial value", - type: "option", - value: NUMBERS - }, - { - name: "3rd (middle) index rotor", - type: "editableOption", - value: I_ROTORS, - defaultIndex: 0 - }, - { - name: "3rd index rotor intial value", - type: "option", - value: NUMBERS - }, - { - name: "4th index rotor", - type: "editableOption", - value: I_ROTORS, - defaultIndex: 0 - }, - { - name: "4th index rotor intial value", - type: "option", - value: NUMBERS - }, - { - name: "5th (right-hand) index rotor", - type: "editableOption", - value: I_ROTORS, - defaultIndex: 0 - }, - { - name: "5th index rotor intial value", - type: "option", - value: NUMBERS - }, - { - name: "SIGABA mode", - type: "option", - value: ["Encrypt", "Decrypt"] - } - ]; + this.name = "SIGABA"; + this.module = "SIGABA"; + this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex than other rotor machines of its time, such as Enigma. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; + this.infoURL = "https://en.wikipedia.org/wiki/SIGABA"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "1st (left-hand) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "1st cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "1st cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "2nd cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "2nd cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "3rd (middle) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "3rd cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "4th cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "4th cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "4th cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "5th (right-hand) cipher rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "5th cipher rotor reversed", + type: "boolean", + value: false + }, + { + name: "5th cipher rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "1st (left-hand) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "1st control rotor reversed", + type: "boolean", + value: false + }, + { + name: "1st control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "2nd control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd control rotor reversed", + type: "boolean", + value: false + }, + { + name: "2nd control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "3rd (middle) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd control rotor reversed", + type: "boolean", + value: false + }, + { + name: "3rd control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "4th control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "4th control rotor reversed", + type: "boolean", + value: false + }, + { + name: "4th control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "5th (right-hand) control rotor", + type: "editableOption", + value: CR_ROTORS, + defaultIndex: 0 + }, + { + name: "5th control rotor reversed", + type: "boolean", + value: false + }, + { + name: "5th control rotor intial value", + type: "option", + value: LETTERS + }, + { + name: "1st (left-hand) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "1st index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "2nd index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "2nd index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "3rd (middle) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "3rd index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "4th index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "4th index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "5th (right-hand) index rotor", + type: "editableOption", + value: I_ROTORS, + defaultIndex: 0 + }, + { + name: "5th index rotor intial value", + type: "option", + value: NUMBERS + }, + { + name: "SIGABA mode", + type: "option", + value: ["Encrypt", "Decrypt"] + } + ]; } /** - @param {string} rotor - rotor wirings + @param {string} input + @param {Object[]} args @returns {string} */ - - parseRotorStr(rotor){ - if (rotor === ""){ - throw new OperationError(`All rotor wirings must be provided.`); - } - return rotor; - } - - run(input, args){ + run(input, args) { const sigabaSwitch = args[40]; const cipherRotors = []; const controlRotors = []; const indexRotors = []; - for (let i=0; i<5; i++){ - const rotorWiring = this.parseRotorStr(args[i*3]); + for (let i=0; i<5; i++) { + const rotorWiring = args[i*3]; cipherRotors.push(new CRRotor(rotorWiring, args[i*3+2], args[i*3+1])); } - for (let i=5; i<10; i++){ - const rotorWiring = this.parseRotorStr(args[i*3]); + for (let i=5; i<10; i++) { + const rotorWiring = args[i*3]; controlRotors.push(new CRRotor(rotorWiring, args[i*3+2], args[i*3+1])); } - for (let i=15; i<20; i++){ - const rotorWiring = this.parseRotorStr(args[i*2]); + for (let i=15; i<20; i++) { + const rotorWiring = args[i*2]; indexRotors.push(new IRotor(rotorWiring, args[i*2+1])); } const sigaba = new SigabaMachine(cipherRotors, controlRotors, indexRotors); - var result; - if (sigabaSwitch === "Encrypt"){ + let result; + if (sigabaSwitch === "Encrypt") { result = sigaba.encrypt(input); - } - else if (sigabaSwitch === "Decrypt") { + } else if (sigabaSwitch === "Decrypt") { result = sigaba.decrypt(input); } return result; From e2b3389da687e74896c0d0ee8e6e89e40141ec9f Mon Sep 17 00:00:00 2001 From: hettysymes <59455170+hettysymes@users.noreply.github.com> Date: Sun, 12 Jan 2020 17:57:20 +0000 Subject: [PATCH 039/686] Added SIGABA simple test --- src/core/config/Categories.json | 3 +- tests/operations/tests/SIGABA.mjs | 67 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) mode change 100755 => 100644 src/core/config/Categories.json create mode 100644 tests/operations/tests/SIGABA.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json old mode 100755 new mode 100644 index 77e3d319..aee80ed4 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -116,7 +116,8 @@ "Multiple Bombe", "Typex", "Lorenz", - "Colossus" + "Colossus", + "SIGABA" ] }, { diff --git a/tests/operations/tests/SIGABA.mjs b/tests/operations/tests/SIGABA.mjs new file mode 100644 index 00000000..f8b19c9d --- /dev/null +++ b/tests/operations/tests/SIGABA.mjs @@ -0,0 +1,67 @@ +/** +SIGABA machine tests + +@author hettysymes +@copyright hettysymes 2020 +@license Apache-2.0 +*/ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "SIGABA: encrypt", + input: "hello world testing the sigaba machine", + expectedOutput: "ULBECJCZJBJFVUDLIXGLGIVXSYGMFRJVCERGOX", + recipeConfig: [ + { + "op": "SIGABA", + "args": [ + "BHKWECJDOVAYLFMITUGXRNSPZQ", true, "G", + "CDTAKGQOZXLVJYHSWMIBPRUNEF", false, "L", + "WAXHJZMBVDPOLTUYRCQFNSGKEI", false, "I", + "HUSCWIMJQXDALVGBFTOYZKRPNE", false, "T", + "RTLSMNKXFVWQUZGCHEJBYDAIPO", false, "B", + "GHAQBRJWDMNZTSKLOUXYPFIECV", false, "N", + "VFLGEMTCXZIQDYAKRPBONHWSUJ", true, "Q", + "ZQCAYHRJNXPFLKIOTBUSVWMGDE", false, "B", + "EZVSWPCTULGAOFDJNBIYMXKQHR", false, "J", + "ELKSGDXMVYJUZNCAROQBPWHITF", false, "R", + "3891625740", "3", + "6297135408", "1", + "2389715064", "8", + "9264351708", "6", + "9573086142", "6", + "Encrypt" + ] + } + ] + }, + { + name: "SIGABA: decrypt", + input: "helloxworldxtestingxthexsigabaxmachine", + expectedOutput: "XWCIWSAIQKNPBUKAP QXVYW RRNYAWXKRBGCQS", + recipeConfig: [ + { + "op": "SIGABA", + "args": [ + "ZECIPSQVBYKJTNRLOXUFGAWHMD", false, "C", + "IPHECDYSZTRXQUKWNVGOBLFJAM", true, "J", + "YHXUSRKIJVQWTPLAZOMDCGNEFB", true, "Z", + "TDPVSOBXULANZQYEHIGFMCRWJK", false, "W", + "THZGFXQRVBSDUICNYJWPAEMOKL", false, "F", + "KOVUTBMZQWGYDNAICSPHERXJLF", false, "F", + "DSTRLAUFXGWCEOKQPVMBZNIYJH", true, "A", + "KCULNSIXJDPEHGQYRTFZVWOBAM", false, "H", + "DZANEQLOWYRXKGUSIVJFMPBCHT", true, "M", + "MVRLHTPFWCAOKEGXZBJYIQUNSD", false, "E", + "9421765830", "3", + "3476815902", "2", + "5701842693", "7", + "4178920536", "0", + "5243709861", "1", + "Decrypt" + ] + } + ] + } +]); From 3c68ad13024b8e08f8f02e26504d6de6f019cc58 Mon Sep 17 00:00:00 2001 From: hettysymes Date: Sun, 7 Jun 2020 17:45:17 +0100 Subject: [PATCH 040/686] Modified control rotor stepping so the next control rotor steps once the previous rotor reaches "O" and added tests --- src/core/lib/SIGABA.mjs | 13 +++--- tests/operations/index.mjs | 1 + tests/operations/tests/SIGABA.mjs | 74 ++++++++++++++++++++----------- 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/core/lib/SIGABA.mjs b/src/core/lib/SIGABA.mjs index b69c7739..30166ad4 100644 --- a/src/core/lib/SIGABA.mjs +++ b/src/core/lib/SIGABA.mjs @@ -216,7 +216,6 @@ export class ControlBank { */ constructor(rotors) { this.rotors = [...rotors].reverse(); - this.numberOfMoves = 1; } /** @@ -258,14 +257,14 @@ export class ControlBank { */ step() { const MRotor = this.rotors[1], FRotor = this.rotors[2], SRotor = this.rotors[3]; - this.numberOfMoves ++; - FRotor.step(); - if (this.numberOfMoves%26 === 0) { + // 14 is the offset of "O" from "A" - the next rotor steps once the previous rotor reaches "O" + if (FRotor.state === 14) { + if (MRotor.state === 14) { + SRotor.step(); + } MRotor.step(); } - if (this.numberOfMoves%(26*26) === 0) { - SRotor.step(); - } + FRotor.step(); } /** diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 8d3cd623..832b9ddd 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -101,6 +101,7 @@ import "./tests/LuhnChecksum.mjs"; import "./tests/CipherSaber2.mjs"; import "./tests/Colossus.mjs"; import "./tests/ParseObjectIDTimestamp.mjs"; +import "./tests/SIGABA.mjs"; // Cannot test operations that use the File type yet diff --git a/tests/operations/tests/SIGABA.mjs b/tests/operations/tests/SIGABA.mjs index f8b19c9d..7bf196be 100644 --- a/tests/operations/tests/SIGABA.mjs +++ b/tests/operations/tests/SIGABA.mjs @@ -9,9 +9,9 @@ import TestRegister from "../../lib/TestRegister.mjs"; TestRegister.addTests([ { - name: "SIGABA: encrypt", - input: "hello world testing the sigaba machine", - expectedOutput: "ULBECJCZJBJFVUDLIXGLGIVXSYGMFRJVCERGOX", + name: "SIGABA: encrypt test 1", + input: "HELLO WORLD TESTING THE SIGABA MACHINE", + expectedOutput: "ULBECJCZJBJFVUDWAVRGRBMPSQHOTTNVQEESKN", recipeConfig: [ { "op": "SIGABA", @@ -37,30 +37,54 @@ TestRegister.addTests([ ] }, { - name: "SIGABA: decrypt", - input: "helloxworldxtestingxthexsigabaxmachine", - expectedOutput: "XWCIWSAIQKNPBUKAP QXVYW RRNYAWXKRBGCQS", + name: "SIGABA: encrypt test 2", + input: "PCRPJZWSPNOHMWANBFBEIVZOXDQESPYDEFBNTHXLSICIRPKUATJVDUQFLZOKGHHHDUDIBRKUHVCGAGLBWVGFFXNDHKPFSPSCIIPCXUFRRHNYWIJFEJWQSGMSNJHWSLPKVXHUQUWIURHDIHIUTWGQFIYLTKEZAUESWYEKIWXUSSXWXBEHCXCUDQWKCISVPKXJVPOIJZWTUGKAORBMKBAQUZOPTSUSYZRROWQUYKNCLHVIHEGWCCONGVHEKCEXVYIPNILIXTXDELNGLJGMEQKKQJWZLPNXPOGIOSVAEAJYKWYJXXGKKPLVYAZGDCMNHMPLCYWDQSRBEMVVVZVFYJMRYGHJOTDOEQVRQOVXOGOVYGTXETFHAYELRYVDGWOFVGAOWPMHQYRZMNXVTAHWSKZLJDFVQPZGMHZWFNOBHSZHEDAEXIFCEEJYZDOEFOQWCXTKPJRUEITKHVCITCLKBUFNAFBYXELAYPBRGGGOCCAGLXXJXTSWCJHMHQPVUIBAGBDKAGEEEPKRGGICJQXSYHBNNAKGYODRAUWAEYHWCKHEQIBAONWQJYQCIFKDTOCTJMBJULWKMSNNMPXINHZQWUMJQLQKIPVZVRGYPCJJZMENWTFTUSPCSPRXHMZPCHCNQTUDCOUJHRKYQIUWWVEVVRYFDIYRQISNGPMQLNMCNMVBEWHNCUODHAGEVEUMKVZLEIKYAMPGVVSBYNRJMFCATDXTQCYXIBCXXKYEYHHYERQGQWZTWCEJBFQLRFFCIVVSZUKGLOTLNGLQNTIKTBBWVFMONUFKRLCJASEKUEEDDQDIVQMFRSJRNHYZJODFHSCJSDAIRUXOSDNFUFUFMNZYQIEGRUXKUPCHENUEZHRKYHDRJYSHLZNYRBWVXORMJMJRIRNSAJQRUMPCXUDFYRGKEAXQXJHPEWNIYIDURDGWIFEMSOFYYCFRZGMZXJNTLTJBBSZIULQSOMEVGCTCVXUHTIEHSPOPQYCJLPAJAPQPAQXE", + expectedOutput: "GMEXPPCMFGKUVGXZHVTCKXRSTJUYWNOKFVELWAHHSJBXGOEXCMLOVSIMCDMGEYMWWTFDUMCDUJEZITNPVVBGQDJEVHJXSKJAAUZWBELMSPUTXCUYPDTJCQXEBGWPWRSQLSNFMASCTJZDSFNKDDTAXLRGUPKCBNXMZPADJSFGGNYKRPYBNTYPTGVPACBEINILNACWFVKMJPGCEZFROEYYKTGYSQYMFSGVDOJJONNYEYSCCIXWLKUSJZDRVAQSNUWHMDJVDNNMPGOYRGQRSBGSPQKGCTFZQWSOXBWSQZDCRQJQAWZDPQEILGMMABIMCDPNSKAFCLPQGIRJCMGQREBEUHBYREXFABFMVZTZBDUMASVNUMHIYRSZLGNZFMVAIABLCUZLJLKKZPWEXDHYZFVSNRLCLNDRKLKSWRHQVQJRTHCNFZXDEXSLAXXOGMFVSGCJGAWOLGDMTLWSFNTCUVCCEACINRZAZZOGLEHHXLPHVKILBBJDPOOCILQKKGODSXOBDPZZDXHJLLBOBVFCHJVMUBUZZIKGCWGCYGXVEHHIJGPEQERWEZLILQNHPHALFKFMGADNELGBKILKIUETGDCBQUEOECWVFNOXTJKUYPWBNEKYSIKMVSAMBZGLIKDAOELRSTKFASEKABTUCPSFEGXXQGDFPSPVOLBHGLZSLLWCABSRKZDQQRKVCKXDGTIHPDNMPDZEXYFYKXZTPJPLYOFNLWAGKJEOHOYLMZELXIDWWNXPKEPUCKNNNHJLFYHPQNHMMCGMUPHSUSYYIVWTIMFKKKTFPGFTLTWWSQBRBMGBTZXPVULKNZIIKVTYLJFISGPTLZFTCLGNZOMVKZOIMUDGXRDDSVFRHRYWBEWHYLCUISYMRWAZZAQPJYXZQQKZLILOSHXUTQJFPTXQSREKSUDZTLGUDLUGOJMQHJRJHXCHQTKJULTWWQOXIRFRQEYBPJPEKXFIRMNATWNFBADOSIJVZYRYDBHDAEDJUVDHLDAU", recipeConfig: [ - { - "op": "SIGABA", + { "op": "SIGABA", "args": [ - "ZECIPSQVBYKJTNRLOXUFGAWHMD", false, "C", - "IPHECDYSZTRXQUKWNVGOBLFJAM", true, "J", - "YHXUSRKIJVQWTPLAZOMDCGNEFB", true, "Z", - "TDPVSOBXULANZQYEHIGFMCRWJK", false, "W", - "THZGFXQRVBSDUICNYJWPAEMOKL", false, "F", - "KOVUTBMZQWGYDNAICSPHERXJLF", false, "F", - "DSTRLAUFXGWCEOKQPVMBZNIYJH", true, "A", - "KCULNSIXJDPEHGQYRTFZVWOBAM", false, "H", - "DZANEQLOWYRXKGUSIVJFMPBCHT", true, "M", - "MVRLHTPFWCAOKEGXZBJYIQUNSD", false, "E", - "9421765830", "3", - "3476815902", "2", - "5701842693", "7", - "4178920536", "0", - "5243709861", "1", - "Decrypt" - ] + "YCHLQSUGBDIXNZKERPVJTAWFOM", true, "A", + "INPXBWETGUYSAOCHVLDMQKZJFR", false, "B", + "WNDRIOZPTAXHFJYQBMSVEKUCGL", false, "C", + "TZGHOBKRVUXLQDMPNFWCJYEIAS", false, "D", + "YWTAHRQJVLCEXUNGBIPZMSDFOK", true, "E", + "QSLRBTEKOGAICFWYVMHJNXZUDP", false, "F", + "CHJDQIGNBSAKVTUOXFWLEPRMZY", false, "G", + "CDFAJXTIMNBEQHSUGRYLWZKVPO", true, "H", + "XHFESZDNRBCGKQIJLTVMUOYAPW", false, "I", + "EZJQXMOGYTCSFRIUPVNADLHWBK", false, "J", + "7591482630", "0", + "3810592764", "1", + "4086153297", "2", + "3980526174", "3", + "6497135280", "4", + "Encrypt"] + } + ] + }, + { + name: "SIGABA: decrypt test", + input: "AKDHFWAYSLHJDKXEVMJJHGKFTQBZPJPJILOVHMBYOAGBZVLLTQUOIKXFPUFNILBDPCAELMAPSXTLMUEGSDTNUDWGZDADBFELWWHKVPRZNDATDPYEHIDMTGAGPDEZYXFSASVKSBMXVOJQXRMHDBWUNZDTIIIVKHJYPIEUHAJCNBXNLGVFADEWIKXDJZBUTGOQBCQZWYKRVEENWRWWRYDNOAPGMODTPTUJZCLUCRDILJABNTBTWUEIJSJRQBUVCOUJJDWFMNNUHXBDFYXLGUMXQEAWSVHBXQGEOOGPYRVOAJLAIYIOHHEXACDTAWWCBGQRNPERSIKHTXPXKBUNACZLFZTRBMBBDDGKNBIQMFHZROCZZBGNZSJKDRRWPEQHLCFADNPWPWSLPIFNKBWQPMARUERGWUUODXSCOJQECGHIZRFRNRSXWSFWKISHHTUFRVXLHCQWGBMRDHCYDSVNIDDRSTODCGJSSBLUYOBGEWFOVKOZBJTYCAKMZECUGLJGTSZJNBOLTMUZRRSIGGRQHLRPMGLINASSMZOBNACKUMSFNIZAUFCPFXXOOTJQWWLZOFLGZLHJCWZJCRJKVOUDLNMKQATGVTOFHACAEKFLRWRTTMVRXHYGOTYPNBMUSKDAKXFCICUOVSWXGPQOYUUWTWRPQMEQCSDJMMJKELIHGEDYKWOVHVPUAIBFGAODXODXVFIIZIGWRZSBTIGXVHFABMMOPGVMLGHQQXNOEJRDLOBGUOWSELBHERZFSBLUODMOGIBNVGVGQYDBTKLOPNKZZNGLTTGZYYXIBAHZJDCILZXKNSJDHXWTYQLFHTUINTYSBPIXOPLOQHSAHGQPYUWYNPKMRBBBYIICCBBJRKWVLBIDBBEKJCXHLPUBMIGBUFYDPOCSRUNZOKMKJHMYFJZWFNHQZOGGRTNNUVLMRLDSAJIECTYCJKBYVNAXGCMGNVFJEDSATZQDQTYRBPLZKHAXMOVJZEDKINXKBUVWXXHTYUFO", + expectedOutput: "KTSOYDGMLPMVXEAJIATXCNQFXHBNCBXIJOCQGCQBRQSBYFOOEVPVXACBMIUIRNVMJHREKRHBSXJFSMWCKTTCYXJOFSJCQECXXCHTEGPEYSMYDHCSMODUAVBNLILYUIBBIXJCXXNQPCERRSMJTPQLMOXSKTRPWOFUSWXOYRJLBIJGIOYTEAEJEGGYAGSXNHNQTETANPWEGATHSBFLHCVHVIJUAKDVGQCWUSIFFFVAJYPJAFUYDXSLGPGESOUAYXBQIIOXWTXNOXLNCGWSUKVIBMOUGNHORYLSNVNNJLKKFDUAEISOLBLCXYHMDGVBVVVIKDLTMTDVWWJBXWXROVTJBXXKXLEWTTISKIUMYSACVUGGNANMCGUMFNQUXDLTHJNYTFIQEPKQQQSSROYJOILJYQXICXACWGOHCSHENXJILOMIIFCIOUDXDCINIVKIRJCVHWXSFQXMNRBJJWTPXNJADEOPEJBLKHKXNTORIRVRLXUXXAMKMODBXNLQCVJXVOTBRHXBBVJHPFEQFCRXYRRXHXPTXXSUESUTHUGOWQYQPQFPXQPVGEIRPQNKXXMBHIPECRUWFEWJUTYIKSMJSRQIQAIAMXTGDXSJIABHIGKUPJBCHWMVYTMQNQYGDHCNMBSVTPXNFRELFXXQYIOLCDEXDXDVSINICOXRMNSPICPQMOBIDJCNBJKXFAVMUXOXHERJIBIXLMXXULDXKXXHAQDXEXIWXOEEUGKSUGCMRWJDPYCYKXTPCOXMURAJCPRXKFJAJALERWRHVMFHOGMFHXGSXQDPJCJNXRQFGHKRCYTEBJDHPCMYFEAPWSVVMMBVUJJMCAAYURHUPVQVJYDCSNMQEMNIFEXYXIIXBVRVILXAUCBDXRJHGPKPYXHPPPNVSBBCDRLVVIYPKAKYIXTJVYDGVPHXULWMADBEICNIFKWUOOHEFNANDKOXMCVBVORLQYNXLULOEGVGWNKNMOHYVRSYSOVYGAKCGAWKGAIAQNQR", + recipeConfig: [ + { "op": "SIGABA", + "args": [ + "YCHLQSUGBDIXNZKERPVJTAWFOM", true, "A", + "INPXBWETGUYSAOCHVLDMQKZJFR", false, "B", + "WNDRIOZPTAXHFJYQBMSVEKUCGL", false, "C", + "TZGHOBKRVUXLQDMPNFWCJYEIAS", false, "D", + "YWTAHRQJVLCEXUNGBIPZMSDFOK", true, "E", + "QSLRBTEKOGAICFWYVMHJNXZUDP", false, "F", + "CHJDQIGNBSAKVTUOXFWLEPRMZY", false, "G", + "CDFAJXTIMNBEQHSUGRYLWZKVPO", true, "H", + "XHFESZDNRBCGKQIJLTVMUOYAPW", false, "I", + "EZJQXMOGYTCSFRIUPVNADLHWBK", false, "J", + "7591482630", "0", + "3810592764", "1", + "4086153297", "2", + "3980526174", "3", + "6497135280", "4", + "Decrypt"] } ] } From 88947b9d42bd9e4ae085406db3f3301385da68ac Mon Sep 17 00:00:00 2001 From: hettysymes Date: Mon, 8 Jun 2020 12:27:40 +0100 Subject: [PATCH 041/686] Added operation description note and modified comment formatting --- src/core/lib/SIGABA.mjs | 338 +++++++++++++++--------------- src/core/operations/SIGABA.mjs | 31 +-- tests/operations/tests/SIGABA.mjs | 12 +- 3 files changed, 193 insertions(+), 188 deletions(-) diff --git a/src/core/lib/SIGABA.mjs b/src/core/lib/SIGABA.mjs index 30166ad4..09951c4f 100644 --- a/src/core/lib/SIGABA.mjs +++ b/src/core/lib/SIGABA.mjs @@ -1,15 +1,14 @@ /** -Emulation of the SIGABA machine - -@author hettysymes -@copyright hettysymes 2020 -@license Apache-2.0 -*/ + * Emulation of the SIGABA machine + * + * @author hettysymes + * @copyright hettysymes 2020 + * @license Apache-2.0 + */ /** -A set of randomised example SIGABA cipher/control rotors (these rotors are interchangeable). Cipher and control rotors can be referred to as C and R rotors respectively. -*/ - + * A set of randomised example SIGABA cipher/control rotors (these rotors are interchangeable). Cipher and control rotors can be referred to as C and R rotors respectively. + */ export const CR_ROTORS = [ {name: "Example 1", value: "SRGWANHPJZFXVIDQCEUKBYOLMT"}, {name: "Example 2", value: "THQEFSAZVKJYULBODCPXNIMWRG"}, @@ -24,9 +23,8 @@ export const CR_ROTORS = [ ]; /** -A set of randomised example SIGABA index rotors (may be referred to as I rotors). -*/ - + * A set of randomised example SIGABA index rotors (may be referred to as I rotors). + */ export const I_ROTORS = [ {name: "Example 1", value: "6201348957"}, {name: "Example 2", value: "6147253089"}, @@ -38,11 +36,11 @@ export const I_ROTORS = [ export const NUMBERS = "0123456789".split(""); /** -Converts a letter to uppercase (if it already isn't) - -@param {char} letter - letter to convert to upper case -@returns {char} -*/ + * Converts a letter to uppercase (if it already isn't) + * + * @param {char} letter - letter to convert to uppercase + * @returns {char} + */ export function convToUpperCase(letter) { const charCode = letter.charCodeAt(); if (97<=charCode && charCode<=122) { @@ -52,16 +50,17 @@ export function convToUpperCase(letter) { } /** -The SIGABA machine consisting of the 3 rotor banks: cipher, control and index banks. -*/ + * The SIGABA machine consisting of the 3 rotor banks: cipher, control and index banks. + */ export class SigabaMachine { - /** - SigabaMachine constructor - @param {Object[]} cipherRotors - list of CRRotors - @param {Object[]} controlRotors - list of CRRotors - @param {object[]} indexRotors - list of IRotors - */ + /** + * SigabaMachine constructor + * + * @param {Object[]} cipherRotors - list of CRRotors + * @param {Object[]} controlRotors - list of CRRotors + * @param {object[]} indexRotors - list of IRotors + */ constructor(cipherRotors, controlRotors, indexRotors) { this.cipherBank = new CipherBank(cipherRotors); this.controlBank = new ControlBank(controlRotors); @@ -69,8 +68,8 @@ export class SigabaMachine { } /** - Steps all the correct rotors in the machine. - */ + * Steps all the correct rotors in the machine. + */ step() { const controlOut = this.controlBank.goThroughControl(); const indexOut = this.indexBank.goThroughIndex(controlOut); @@ -78,11 +77,11 @@ export class SigabaMachine { } /** - Encrypts a letter. A space is converted to a "Z" before encryption, and a "Z" is converted to an "X". This allows spaces to be encrypted. - - @param {char} letter - letter to encrypt - @returns {char} - */ + * Encrypts a letter. A space is converted to a "Z" before encryption, and a "Z" is converted to an "X". This allows spaces to be encrypted. + * + * @param {char} letter - letter to encrypt + * @returns {char} + */ encryptLetter(letter) { letter = convToUpperCase(letter); if (letter === " ") { @@ -96,11 +95,11 @@ export class SigabaMachine { } /** - Decrypts a letter. A letter decrypted as a "Z" is converted to a space before it is output, since spaces are converted to "Z"s before encryption. - - @param {char} letter - letter to decrypt - @returns {char} - */ + * Decrypts a letter. A letter decrypted as a "Z" is converted to a space before it is output, since spaces are converted to "Z"s before encryption. + * + * @param {char} letter - letter to decrypt + * @returns {char} + */ decryptLetter(letter) { letter = convToUpperCase(letter); let decryptedLetter = this.cipherBank.decrypt(letter); @@ -112,11 +111,11 @@ export class SigabaMachine { } /** - Encrypts a message of one or more letters - - @param {string} msg - message to encrypt - @returns {string} - */ + * Encrypts a message of one or more letters + * + * @param {string} msg - message to encrypt + * @returns {string} + */ encrypt(msg) { let ciphertext = ""; for (const letter of msg) { @@ -126,11 +125,11 @@ export class SigabaMachine { } /** - Decrypts a message of one or more letters - - @param {string} msg - message to decrypt - @returns {string} - */ + * Decrypts a message of one or more letters + * + * @param {string} msg - message to decrypt + * @returns {string} + */ decrypt(msg) { let plaintext = ""; for (const letter of msg) { @@ -142,24 +141,25 @@ export class SigabaMachine { } /** -The cipher rotor bank consists of 5 cipher rotors in either a forward or reversed orientation. -*/ + * The cipher rotor bank consists of 5 cipher rotors in either a forward or reversed orientation. + */ export class CipherBank { - /** - CipherBank constructor - @param {Object[]} rotors - list of CRRotors - */ + /** + * CipherBank constructor + * + * @param {Object[]} rotors - list of CRRotors + */ constructor(rotors) { this.rotors = rotors; } /** - Encrypts a letter through the cipher rotors (signal goes from left-to-right) - - @param {char} inputPos - the input position of the signal (letter to be encrypted) - @returns {char} - */ + * Encrypts a letter through the cipher rotors (signal goes from left-to-right) + * + * @param {char} inputPos - the input position of the signal (letter to be encrypted) + * @returns {char} + */ encrypt(inputPos) { for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos, "leftToRight"); @@ -168,11 +168,11 @@ export class CipherBank { } /** - Decrypts a letter through the cipher rotors (signal goes from right-to-left) - - @param {char} inputPos - the input position of the signal (letter to be decrypted) - @returns {char} - */ + * Decrypts a letter through the cipher rotors (signal goes from right-to-left) + * + * @param {char} inputPos - the input position of the signal (letter to be decrypted) + * @returns {char} + */ decrypt(inputPos) { const revOrderedRotors = [...this.rotors].reverse(); for (const rotor of revOrderedRotors) { @@ -182,10 +182,10 @@ export class CipherBank { } /** - Step the cipher rotors forward according to the inputs from the index rotors - - @param {number[]} indexInputs - the inputs from the index rotors - */ + * Step the cipher rotors forward according to the inputs from the index rotors + * + * @param {number[]} indexInputs - the inputs from the index rotors + */ step(indexInputs) { const logicDict = {0: [0, 9], 1: [7, 8], 2: [5, 6], 3: [3, 4], 4: [1, 2]}; const rotorsToMove = []; @@ -206,24 +206,25 @@ export class CipherBank { } /** -The control rotor bank consists of 5 control rotors in either a forward or reversed orientation. Signals to the control rotor bank always go from right-to-left. -*/ + * The control rotor bank consists of 5 control rotors in either a forward or reversed orientation. Signals to the control rotor bank always go from right-to-left. + */ export class ControlBank { - /** - ControlBank constructor. The rotors have been reversed as signals go from right-to-left through the control rotors. - @param {Object[]} rotors - list of CRRotors - */ + /** + * ControlBank constructor. The rotors have been reversed as signals go from right-to-left through the control rotors. + * + * @param {Object[]} rotors - list of CRRotors + */ constructor(rotors) { this.rotors = [...rotors].reverse(); } /** - Encrypts a letter. - - @param {char} inputPos - the input position of the signal - @returns {char} - */ + * Encrypts a letter. + * + * @param {char} inputPos - the input position of the signal + * @returns {char} + */ crypt(inputPos) { for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos, "rightToLeft"); @@ -232,10 +233,10 @@ export class ControlBank { } /** - Gets the outputs of the control rotors. The inputs to the control rotors are always "F", "G", "H" and "I". - - @returns {number[]} - */ + * Gets the outputs of the control rotors. The inputs to the control rotors are always "F", "G", "H" and "I". + * + * @returns {number[]} + */ getOutputs() { const outputs = [this.crypt("F"), this.crypt("G"), this.crypt("H"), this.crypt("I")]; const logicDict = {1: "B", 2: "C", 3: "DE", 4: "FGH", 5: "IJK", 6: "LMNO", 7: "PQRST", 8: "UVWXYZ", 9: "A"}; @@ -253,8 +254,8 @@ export class ControlBank { } /** - Steps the control rotors. Only 3 of the control rotors step: one after every encryption, one after every 26, and one after every 26 squared. - */ + * Steps the control rotors. Only 3 of the control rotors step: one after every encryption, one after every 26, and one after every 26 squared. + */ step() { const MRotor = this.rotors[1], FRotor = this.rotors[2], SRotor = this.rotors[3]; // 14 is the offset of "O" from "A" - the next rotor steps once the previous rotor reaches "O" @@ -268,10 +269,10 @@ export class ControlBank { } /** - The goThroughControl function combines getting the outputs from the control rotor bank and then stepping them. - - @returns {number[]} - */ + * The goThroughControl function combines getting the outputs from the control rotor bank and then stepping them. + * + * @returns {number[]} + */ goThroughControl() { const outputs = this.getOutputs(); this.step(); @@ -281,24 +282,25 @@ export class ControlBank { } /** -The index rotor bank consists of 5 index rotors all placed in the forwards orientation. -*/ + * The index rotor bank consists of 5 index rotors all placed in the forwards orientation. + */ export class IndexBank { - /** - IndexBank constructor - @param {Object[]} rotors - list of IRotors - */ + /** + * IndexBank constructor + * + * @param {Object[]} rotors - list of IRotors + */ constructor(rotors) { this.rotors = rotors; } /** - Encrypts a number. - - @param {number} inputPos - the input position of the signal - @returns {number} - */ + * Encrypts a number. + * + * @param {number} inputPos - the input position of the signal + * @returns {number} + */ crypt(inputPos) { for (const rotor of this.rotors) { inputPos = rotor.crypt(inputPos); @@ -307,11 +309,11 @@ export class IndexBank { } /** - The goThroughIndex function takes the inputs from the control rotor bank and returns the list of outputs after encryption through the index rotors. - - @param {number[]} - inputs from the control rotors - @returns {number[]} - */ + * The goThroughIndex function takes the inputs from the control rotor bank and returns the list of outputs after encryption through the index rotors. + * + * @param {number[]} controlInputs - inputs from the control rotors + * @returns {number[]} + */ goThroughIndex(controlInputs) { const outputs = []; for (const inp of controlInputs) { @@ -323,16 +325,17 @@ export class IndexBank { } /** -Rotor class -*/ + * Rotor class + */ export class Rotor { - /** - Rotor constructor - @param {number[]} wireSetting - the wirings within the rotor: mapping from left-to-right, the index of the number in the list maps onto the number at that index - @param {bool} rev - true if the rotor is reversed, false if it isn't - @param {number} key - the starting position or state of the rotor - */ + /** + * Rotor constructor + * + * @param {number[]} wireSetting - the wirings within the rotor: mapping from left-to-right, the index of the number in the list maps onto the number at that index + * @param {bool} rev - true if the rotor is reversed, false if it isn't + * @param {number} key - the starting position or state of the rotor + */ constructor(wireSetting, key, rev) { this.state = key; this.numMapping = this.getNumMapping(wireSetting, rev); @@ -340,12 +343,12 @@ export class Rotor { } /** - Get the number mapping from the wireSetting (only different from wireSetting if rotor is reversed) - - @param {number[]} wireSetting - the wirings within the rotors - @param {bool} rev - true if reversed, false if not - @returns {number[]} - */ + * Get the number mapping from the wireSetting (only different from wireSetting if rotor is reversed) + * + * @param {number[]} wireSetting - the wirings within the rotors + * @param {bool} rev - true if reversed, false if not + * @returns {number[]} + */ getNumMapping(wireSetting, rev) { if (rev===false) { return wireSetting; @@ -360,11 +363,11 @@ export class Rotor { } /** - Get the position mapping (how the position numbers map onto the numbers of the rotor) - - @param {bool} rev - true if reversed, false if not - @returns {number[]} - */ + * Get the position mapping (how the position numbers map onto the numbers of the rotor) + * + * @param {bool} rev - true if reversed, false if not + * @returns {number[]} + */ getPosMapping(rev) { const length = this.numMapping.length; const posMapping = []; @@ -389,12 +392,12 @@ export class Rotor { } /** - Encrypt/decrypt data. This process is identical to the rotors of cipher machines such as Enigma or Typex. - - @param {number} inputPos - the input position of the signal (the data to encrypt/decrypt) - @param {string} direction - one of "leftToRight" and "rightToLeft", states the direction in which the signal passes through the rotor - @returns {number} - */ + * Encrypt/decrypt data. This process is identical to the rotors of cipher machines such as Enigma or Typex. + * + * @param {number} inputPos - the input position of the signal (the data to encrypt/decrypt) + * @param {string} direction - one of "leftToRight" and "rightToLeft", states the direction in which the signal passes through the rotor + * @returns {number} + */ cryptNum(inputPos, direction) { const inpNum = this.posMapping[inputPos]; let outNum; @@ -408,8 +411,8 @@ export class Rotor { } /** - Steps the rotor. The number at position 0 will be moved to position 1 etc. - */ + * Steps the rotor. The number at position 0 will be moved to position 1 etc. + */ step() { const lastNum = this.posMapping.pop(); this.posMapping.splice(0, 0, lastNum); @@ -419,49 +422,49 @@ export class Rotor { } /** -A CRRotor is a cipher (C) or control (R) rotor. These rotors are identical and interchangeable. A C or R rotor consists of 26 contacts, one for each letter, and may be put into either a forwards of reversed orientation. -*/ + * A CRRotor is a cipher (C) or control (R) rotor. These rotors are identical and interchangeable. A C or R rotor consists of 26 contacts, one for each letter, and may be put into either a forwards of reversed orientation. + */ export class CRRotor extends Rotor { /** - CRRotor constructor - - @param {string} wireSetting - the rotor wirings (string of letters) - @param {char} key - initial state of rotor - @param {bool} rev - true if reversed, false if not - */ + * CRRotor constructor + * + * @param {string} wireSetting - the rotor wirings (string of letters) + * @param {char} key - initial state of rotor + * @param {bool} rev - true if reversed, false if not + */ constructor(wireSetting, key, rev=false) { wireSetting = wireSetting.split("").map(CRRotor.letterToNum); super(wireSetting, CRRotor.letterToNum(key), rev); } /** - Static function which converts a letter into its number i.e. its offset from the letter "A" - - @param {char} letter - letter to convert to number - @returns {number} - */ + * Static function which converts a letter into its number i.e. its offset from the letter "A" + * + * @param {char} letter - letter to convert to number + * @returns {number} + */ static letterToNum(letter) { return letter.charCodeAt()-65; } /** - Static function which converts a number (a letter's offset from "A") into its letter - - @param {number} num - number to convert to letter - @returns {char} - */ + * Static function which converts a number (a letter's offset from "A") into its letter + * + * @param {number} num - number to convert to letter + * @returns {char} + */ static numToLetter(num) { return String.fromCharCode(num+65); } /** - Encrypts/decrypts a letter. - - @param {char} inputPos - the input position of the signal ("A" refers to position 0 etc.) - @param {string} direction - one of "leftToRight" and "rightToLeft" - @returns {char} - */ + * Encrypts/decrypts a letter. + * + * @param {char} inputPos - the input position of the signal ("A" refers to position 0 etc.) + * @param {string} direction - one of "leftToRight" and "rightToLeft" + * @returns {char} + */ crypt(inputPos, direction) { inputPos = CRRotor.letterToNum(inputPos); const outPos = this.cryptNum(inputPos, direction); @@ -471,26 +474,27 @@ export class CRRotor extends Rotor { } /** -An IRotor is an index rotor, which consists of 10 contacts each numbered from 0 to 9. Unlike C and R rotors, they cannot be put in the reversed orientation. The index rotors do not step at any point during encryption or decryption. -*/ + * An IRotor is an index rotor, which consists of 10 contacts each numbered from 0 to 9. Unlike C and R rotors, they cannot be put in the reversed orientation. The index rotors do not step at any point during encryption or decryption. + */ export class IRotor extends Rotor { - /** - IRotor constructor - @param {string} wireSetting - the rotor wirings (string of numbers) - @param {char} key - initial state of rotor - */ + /** + * IRotor constructor + * + * @param {string} wireSetting - the rotor wirings (string of numbers) + * @param {char} key - initial state of rotor + */ constructor(wireSetting, key) { wireSetting = wireSetting.split("").map(Number); super(wireSetting, Number(key), false); } /** - Encrypts a number - - @param {number} inputPos - the input position of the signal - @returns {number} - */ + * Encrypts a number + * + * @param {number} inputPos - the input position of the signal + * @returns {number} + */ crypt(inputPos) { return this.cryptNum(inputPos, "leftToRight"); } diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs index d82ee09a..42d1a9f3 100644 --- a/src/core/operations/SIGABA.mjs +++ b/src/core/operations/SIGABA.mjs @@ -1,28 +1,29 @@ /** -Emulation of the SIGABA machine. - -@author hettysymes -@copyright hettysymes 2020 -@license Apache-2.0 -*/ + * Emulation of the SIGABA machine. + * + * @author hettysymes + * @copyright hettysymes 2020 + * @license Apache-2.0 + */ import Operation from "../Operation.mjs"; import {LETTERS} from "../lib/Enigma.mjs"; import {NUMBERS, CR_ROTORS, I_ROTORS, SigabaMachine, CRRotor, IRotor} from "../lib/SIGABA.mjs"; /** -Sigaba operation -*/ + * Sigaba operation + */ class Sigaba extends Operation { + /** - Sigaba constructor - */ + * Sigaba constructor + */ constructor() { super(); this.name = "SIGABA"; this.module = "SIGABA"; - this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex than other rotor machines of its time, such as Enigma. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode."; + this.description = "Encipher/decipher with the WW2 SIGABA machine.

SIGABA, otherwise known as ECM Mark II, was used by the United States for message encryption during WW2 up to the 1950s. It was developed in the 1930s by the US Army and Navy, and has up to this day never been broken. Consisting of 15 rotors: 5 cipher rotors and 10 rotors (5 control rotors and 5 index rotors) controlling the stepping of the cipher rotors, the rotor stepping for SIGABA is much more complex than other rotor machines of its time, such as Enigma. All example rotor wirings are random example sets.

To configure rotor wirings, for the cipher and control rotors enter a string of letters which map from A to Z, and for the index rotors enter a sequence of numbers which map from 0 to 9. Note that encryption is not the same as decryption, so first choose the desired mode.

Note: Whilst this has been tested against other software emulators, it has not been tested against hardware."; this.infoURL = "https://en.wikipedia.org/wiki/SIGABA"; this.inputType = "string"; this.outputType = "string"; @@ -251,10 +252,10 @@ class Sigaba extends Operation { } /** - @param {string} input - @param {Object[]} args - @returns {string} - */ + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ run(input, args) { const sigabaSwitch = args[40]; const cipherRotors = []; diff --git a/tests/operations/tests/SIGABA.mjs b/tests/operations/tests/SIGABA.mjs index 7bf196be..5f07ce20 100644 --- a/tests/operations/tests/SIGABA.mjs +++ b/tests/operations/tests/SIGABA.mjs @@ -1,10 +1,10 @@ /** -SIGABA machine tests - -@author hettysymes -@copyright hettysymes 2020 -@license Apache-2.0 -*/ + * SIGABA machine tests + * + * @author hettysymes + * @copyright hettysymes 2020 + * @license Apache-2.0 + */ import TestRegister from "../../lib/TestRegister.mjs"; TestRegister.addTests([ From f5a7db03cd8ab8bf9e7bbd43ec47ae9c57975a37 Mon Sep 17 00:00:00 2001 From: Benedikt Werner <1benediktwerner@gmail.com> Date: Wed, 10 Jun 2020 15:50:26 +0200 Subject: [PATCH 042/686] Base85: Only require 15 continuous base85 chars --- src/core/operations/FromBase85.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index 9d73baa1..3555b020 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -38,7 +38,7 @@ class FromBase85 extends Operation { pattern: "^\\s*(?:<~)?" + // Optional whitespace and starting marker "[\\s!-uz]*" + // Any amount of base85 characters and whitespace - "[!-uz]{20}" + // At least 20 continoues base85 characters without whitespace + "[!-uz]{15}" + // At least 15 continoues base85 characters without whitespace "[\\s!-uz]*" + // Any amount of base85 characters and whitespace "(?:~>)?\\s*$", // Optional ending marker and whitespace args: ["!-u"], @@ -47,7 +47,7 @@ class FromBase85 extends Operation { pattern: "^" + "[\\s0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]*" + - "[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]{20}" + // At least 20 continoues base85 characters without whitespace + "[0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]{15}" + // At least 15 continoues base85 characters without whitespace "[\\s0-9a-zA-Z.\\-:+=^!/*?&<>()[\\]{}@%$#]*" + "$", args: ["0-9a-zA-Z.\\-:+=^!/*?&<>()[]{}@%$#"], @@ -56,7 +56,7 @@ class FromBase85 extends Operation { pattern: "^" + "[\\s0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]*" + - "[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]{20}" + // At least 20 continoues base85 characters without whitespace + "[0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]{15}" + // At least 15 continoues base85 characters without whitespace "[\\s0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~]*" + "$", args: ["0-9A-Za-z!#$%&()*+\\-;<=>?@^_`{|}~"], From d68c8cb845e7976623f35740de50eb4a5ec29a09 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 6 Jul 2020 10:43:52 +0100 Subject: [PATCH 043/686] Casing Variations --- src/core/config/Categories.json | 1 + src/core/operations/GetAllCasings.mjs | 53 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/core/operations/GetAllCasings.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 77e3d319..1bf0b68a 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -217,6 +217,7 @@ "From Case Insensitive Regex", "Add line numbers", "Remove line numbers", + "Get All Casings", "To Table", "Reverse", "Sort", diff --git a/src/core/operations/GetAllCasings.mjs b/src/core/operations/GetAllCasings.mjs new file mode 100644 index 00000000..33892ffc --- /dev/null +++ b/src/core/operations/GetAllCasings.mjs @@ -0,0 +1,53 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Permutate String operation + */ +class GetAllCasings extends Operation { + + /** + * GetAllCasings constructor + */ + constructor() { + super(); + + this.name = "Get All Casings"; + this.module = "Default"; + this.description = "Outputs all possible casing variations of a string."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const length = input.length; + const max = 1 << length; + input = input.toLowerCase(); + let result = ""; + + for (let i = 0; i < max; i++) { + const temp = input.split(""); + for (let j = 0; j < length; j++) { + if (((i >> j) & 1) === 1) { + temp[j] = temp[j].toUpperCase(); + } + } + result += temp.join("") + "\n"; + } + return result; + } +} + +export default GetAllCasings; From c01ce90e06db1d764f836282aa0e6693831230f5 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 6 Jul 2020 11:20:54 +0100 Subject: [PATCH 044/686] Tests Added --- tests/operations/index.mjs | 2 +- tests/operations/tests/GetAllCasings.mjs | 44 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/operations/tests/GetAllCasings.mjs diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 8d3cd623..33260005 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -101,7 +101,7 @@ import "./tests/LuhnChecksum.mjs"; import "./tests/CipherSaber2.mjs"; import "./tests/Colossus.mjs"; import "./tests/ParseObjectIDTimestamp.mjs"; - +import "./tests/GetAllCasings.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; diff --git a/tests/operations/tests/GetAllCasings.mjs b/tests/operations/tests/GetAllCasings.mjs new file mode 100644 index 00000000..e5c6a25b --- /dev/null +++ b/tests/operations/tests/GetAllCasings.mjs @@ -0,0 +1,44 @@ +/** + * GetAllCasings tests. + * + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "All casings of test", + input: "test", + expectedOutput: "test\nTest\ntEst\nTEst\nteSt\nTeSt\ntESt\nTESt\ntesT\nTesT\ntEsT\nTEsT\nteST\nTeST\ntEST\nTEST\n", + recipeConfig: [ + { + "op": "Get All Casings", + "args": [] + } + ] + }, + { + name: "All casings of t", + input: "t", + expectedOutput: "t\nT\n", + recipeConfig: [ + { + "op": "Get All Casings", + "args": [] + } + ] + }, + { + name: "All casings of null", + input: "", + expectedOutput: "\n", + recipeConfig: [ + { + "op": "Get All Casings", + "args": [] + } + ] + } +]); From 3e3c526a625cabeae64d8f3b61f88d326e98473a Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 6 Jul 2020 16:35:14 +0100 Subject: [PATCH 045/686] Caesar Box Cipher Added --- src/core/config/Categories.json | 1 + src/core/operations/CaesarBoxCipher.mjs | 61 ++++++++++++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/CaesarBoxCipher.mjs | 45 ++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 src/core/operations/CaesarBoxCipher.mjs create mode 100644 tests/operations/tests/CaesarBoxCipher.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 77e3d319..36465ced 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -91,6 +91,7 @@ "Bacon Cipher Decode", "Bifid Cipher Encode", "Bifid Cipher Decode", + "Caesar Box Cipher", "Affine Cipher Encode", "Affine Cipher Decode", "A1Z26 Cipher Encode", diff --git a/src/core/operations/CaesarBoxCipher.mjs b/src/core/operations/CaesarBoxCipher.mjs new file mode 100644 index 00000000..2e4d9830 --- /dev/null +++ b/src/core/operations/CaesarBoxCipher.mjs @@ -0,0 +1,61 @@ +/** + * @author n1073645 [n1073645@gmail.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Caesar Box Cipher operation + */ +class CaesarBoxCipher extends Operation { + + /** + * CaesarBoxCipher constructor + */ + constructor() { + super(); + + this.name = "Caesar Box Cipher"; + this.module = "Ciphers"; + this.description = ""; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Box Height", + type: "number", + value: 1 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const tableHeight = args[0]; + const tableWidth = Math.ceil(input.length / tableHeight); + while (input.indexOf(" ") !== -1) + input = input.replace(" ", ""); + for (let i = 0; i < (tableHeight * tableWidth) - input.length; i++) { + input += "\x00"; + } + let result = ""; + for (let i = 0; i < tableHeight; i++) { + for (let j = i; j < input.length; j += tableHeight) { + if (input.charAt(j) !== "\x00") { + result += input.charAt(j); + } + } + } + return result; + } + +} + +export default CaesarBoxCipher; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 8d3cd623..bd6cd3ed 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -101,6 +101,7 @@ import "./tests/LuhnChecksum.mjs"; import "./tests/CipherSaber2.mjs"; import "./tests/Colossus.mjs"; import "./tests/ParseObjectIDTimestamp.mjs"; +import "./tests/CaesarBoxCipher.mjs"; // Cannot test operations that use the File type yet diff --git a/tests/operations/tests/CaesarBoxCipher.mjs b/tests/operations/tests/CaesarBoxCipher.mjs new file mode 100644 index 00000000..3ccdae66 --- /dev/null +++ b/tests/operations/tests/CaesarBoxCipher.mjs @@ -0,0 +1,45 @@ +/** + * Base58 tests. + * + * @author n1073645 [n1073645@gmail.com] + * + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Caesar Box Cipher: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Caesar Box Cipher", + args: ["1"], + }, + ], + }, + { + name: "Caesar Box Cipher: Hello World!", + input: "Hello World!", + expectedOutput: "Hlodeor!lWl", + recipeConfig: [ + { + op: "Caesar Box Cipher", + args: ["3"], + }, + ], + }, + { + name: "Caesar Box Cipher: Hello World!", + input: "Hlodeor!lWl", + expectedOutput: "HelloWorld!", + recipeConfig: [ + { + op: "Caesar Box Cipher", + args: ["4"], + }, + ], + } +]); From 667dfd820e5e2b93a5daf4258f547d6a0a605a37 Mon Sep 17 00:00:00 2001 From: n1073645 Date: Mon, 6 Jul 2020 16:46:40 +0100 Subject: [PATCH 046/686] info url added --- src/core/operations/CaesarBoxCipher.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/CaesarBoxCipher.mjs b/src/core/operations/CaesarBoxCipher.mjs index 2e4d9830..9c835b4b 100644 --- a/src/core/operations/CaesarBoxCipher.mjs +++ b/src/core/operations/CaesarBoxCipher.mjs @@ -19,8 +19,8 @@ class CaesarBoxCipher extends Operation { this.name = "Caesar Box Cipher"; this.module = "Ciphers"; - this.description = ""; - this.infoURL = ""; + this.description = "Caesar Box Encryption uses a box, a rectangle (or a square), or at least a size W caracterizing its width."; + this.infoURL = "https://www.dcode.fr/caesar-box-cipher"; this.inputType = "string"; this.outputType = "string"; this.args = [ From 6b76b7004a9d832acd4f19803c9990b18288b846 Mon Sep 17 00:00:00 2001 From: thezero Date: Sun, 14 Apr 2019 15:08:10 +0200 Subject: [PATCH 047/686] add button to hide recipe's options --- src/web/HTMLOperation.mjs | 1 + src/web/Manager.mjs | 1 + src/web/waiters/RecipeWaiter.mjs | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs index fe075c48..f46b3ba8 100755 --- a/src/web/HTMLOperation.mjs +++ b/src/web/HTMLOperation.mjs @@ -83,6 +83,7 @@ class HTMLOperation {
pause not_interested + keyboard_arrow_up
 
`; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index e1e07dfd..64dc3a35 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -135,6 +135,7 @@ class Manager { // Recipe this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe); this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe); + this.addDynamicListener(".hide-options", "click", this.recipe.hideOptClick, this.recipe); this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe); this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe); this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe); diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index ba0e7b11..afa3e72b 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -214,6 +214,30 @@ class RecipeWaiter { window.dispatchEvent(this.manager.statechange); } + /** + * Handler for hide-opt click events. + * Updates the icon status. + * + * @fires Manager#statechange + * @param {event} e + */ + hideOptClick(e) { + const icon = e.target; + + if (icon.getAttribute("hide-opt") === "false") { + icon.setAttribute("hide-opt", "true"); + icon.innerText = "keyboard_arrow_down"; + icon.classList.add("hide-options-selected"); + icon.parentNode.previousElementSibling.style.display = "none"; + } else { + icon.setAttribute("hide-opt", "false"); + icon.innerText = "keyboard_arrow_up"; + icon.classList.remove("hide-options-selected"); + icon.parentNode.previousElementSibling.style.display = "grid"; + } + + window.dispatchEvent(this.manager.statechange); + } /** * Handler for disable click events. From 3bb6a40f82e98fbc4cf45c82f75c033725862282 Mon Sep 17 00:00:00 2001 From: thezero Date: Mon, 22 Apr 2019 00:18:52 +0200 Subject: [PATCH 048/686] add button to hide all recipe options --- src/web/Manager.mjs | 1 + src/web/html/index.html | 3 +++ src/web/waiters/ControlsWaiter.mjs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 64dc3a35..493d3a19 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -120,6 +120,7 @@ class Manager { document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls)); document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls)); document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls)); + document.getElementById("hide-icon").addEventListener("click", this.controls.hideRecipeOptClick.bind(this.recipe)); document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls)); this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls); diff --git a/src/web/html/index.html b/src/web/html/index.html index 121f0780..ad940040 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -177,6 +177,9 @@
Recipe + diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 2f2705aa..b051e3ce 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -333,6 +333,36 @@ class ControlsWaiter { } + /** + * Hides the options for all the operations in the current recipe. + */ + hideRecipeOptClick() { + const icon = document.getElementById("hide-icon"); + + if (icon.getAttribute("hide-opt") === "false") { + icon.setAttribute("hide-opt", "true"); + icon.setAttribute("data-original-title", "Show options"); + icon.children[0].innerText = "keyboard_arrow_down"; + Array.from(document.getElementsByClassName("hide-options")).forEach(function(item){ + item.setAttribute("hide-opt", "true"); + item.innerText = "keyboard_arrow_down"; + item.classList.add("hide-options-selected"); + item.parentNode.previousElementSibling.style.display = "none"; + }); + } else { + icon.setAttribute("hide-opt", "false"); + icon.setAttribute("data-original-title", "Hide options"); + icon.children[0].innerText = "keyboard_arrow_up"; + Array.from(document.getElementsByClassName("hide-options")).forEach(function(item){ + item.setAttribute("hide-opt", "false"); + item.innerText = "keyboard_arrow_up"; + item.classList.remove("hide-options-selected"); + item.parentNode.previousElementSibling.style.display = "grid"; + }); + } + } + + /** * Populates the bug report information box with useful technical info. * From ed7baf57f0dbc04e07b1479b1e045b7c307d60c1 Mon Sep 17 00:00:00 2001 From: thezero Date: Wed, 21 Oct 2020 00:17:06 +0200 Subject: [PATCH 049/686] replace "options" with "arguments", invert global hide-icon if needed --- src/web/HTMLOperation.mjs | 2 +- src/web/Manager.mjs | 4 ++-- src/web/html/index.html | 2 +- src/web/waiters/ControlsWaiter.mjs | 26 ++++++++++++------------- src/web/waiters/RecipeWaiter.mjs | 31 +++++++++++++++++++++++------- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs index f46b3ba8..285fe10e 100755 --- a/src/web/HTMLOperation.mjs +++ b/src/web/HTMLOperation.mjs @@ -83,7 +83,7 @@ class HTMLOperation {
pause not_interested - keyboard_arrow_up + keyboard_arrow_up
 
`; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 493d3a19..f7e08aa6 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -120,7 +120,7 @@ class Manager { document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls)); document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls)); document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls)); - document.getElementById("hide-icon").addEventListener("click", this.controls.hideRecipeOptClick.bind(this.recipe)); + document.getElementById("hide-icon").addEventListener("click", this.controls.hideRecipeArgsClick.bind(this.recipe)); document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls)); this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls); @@ -136,7 +136,7 @@ class Manager { // Recipe this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe); this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe); - this.addDynamicListener(".hide-options", "click", this.recipe.hideOptClick, this.recipe); + this.addDynamicListener(".hide-args-icon", "click", this.recipe.hideArgsClick, this.recipe); this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe); this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe); this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe); diff --git a/src/web/html/index.html b/src/web/html/index.html index ad940040..b5cff9f0 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -177,7 +177,7 @@
Recipe - @@ -267,7 +265,7 @@
- +
diff --git a/src/web/static/fonts/MaterialIcons-Regular.ttf b/src/web/static/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..54938737932514a89614069b081163cb34826768 GIT binary patch literal 354228 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 diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 7811144a..625b81f7 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -6,7 +6,24 @@ * @license Apache-2.0 */ -#input-text, +#input-text { + position: relative; + width: 100%; + height: 100%; + margin: 0; + background-color: transparent; +} + +.cm-editor { + height: 100%; +} + +.cm-editor .cm-content { + font-family: var(--fixed-width-font-family); + font-size: var(--fixed-width-font-size); + color: var(--fixed-width-font-colour); +} + #output-text, #output-html { position: relative; @@ -163,14 +180,14 @@ #input-wrapper, #output-wrapper, -#input-wrapper > * , +#input-wrapper > :not(#input-text), #output-wrapper > .textarea-wrapper > div, #output-wrapper > .textarea-wrapper > textarea { height: calc(100% - var(--title-height)); } #input-wrapper.show-tabs, -#input-wrapper.show-tabs > *, +#input-wrapper.show-tabs > :not(#input-text), #output-wrapper.show-tabs, #output-wrapper.show-tabs > .textarea-wrapper > div, #output-wrapper.show-tabs > .textarea-wrapper > textarea { @@ -193,7 +210,9 @@ } .textarea-wrapper textarea, -.textarea-wrapper>div { +.textarea-wrapper #output-text, +.textarea-wrapper #output-html, +.textarea-wrapper #output-highlighter { font-family: var(--fixed-width-font-family); font-size: var(--fixed-width-font-size); color: var(--fixed-width-font-colour); @@ -292,10 +311,6 @@ align-items: center; } -#input-info { - line-height: 15px; -} - .dropping-file { border: 5px dashed var(--drop-file-border-colour) !important; } @@ -458,3 +473,73 @@ cursor: pointer; filter: brightness(98%); } + + +/* Status bar */ + +.cm-status-bar { + font-family: var(--fixed-width-font-family); + font-weight: normal; + font-size: 8pt; + margin: 0 5px; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; +} + +.cm-status-bar i { + font-size: 12pt; + vertical-align: middle; + margin-left: 8px; +} +.cm-status-bar>div>span:first-child i { + margin-left: 0; +} + +/* Dropup Button */ +.cm-status-bar-select-btn { + border: none; + cursor: pointer; +} + +/* The container
- needed to position the dropup content */ +.cm-status-bar-select { + position: relative; + display: inline-block; +} + +/* Dropup content (Hidden by Default) */ +.cm-status-bar-select-content { + display: none; + position: absolute; + bottom: 20px; + right: 0; + background-color: #f1f1f1; + min-width: 200px; + box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +/* Links inside the dropup */ +.cm-status-bar-select-content a { + color: black; + padding: 2px 5px; + text-decoration: none; + display: block; +} + +/* Change color of dropup links on hover */ +.cm-status-bar-select-content a:hover { + background-color: #ddd +} + +/* Show the dropup menu on hover */ +.cm-status-bar-select:hover .cm-status-bar-select-content { + display: block; +} + +/* Change the background color of the dropup button when the dropup content is shown */ +.cm-status-bar-select:hover .cm-status-bar-select-btn { + background-color: #f1f1f1; +} diff --git a/src/web/stylesheets/utils/_overrides.css b/src/web/stylesheets/utils/_overrides.css index c06d3b8c..fa216836 100755 --- a/src/web/stylesheets/utils/_overrides.css +++ b/src/web/stylesheets/utils/_overrides.css @@ -13,7 +13,7 @@ font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url("../static/fonts/MaterialIcons-Regular.woff2") format('woff2'); + src: url("../static/fonts/MaterialIcons-Regular.ttf") format('truetype'); } .material-icons { diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 5a9533f5..426107bb 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -140,7 +140,7 @@ class ControlsWaiter { const params = [ includeRecipe ? ["recipe", recipeStr] : undefined, - includeInput ? ["input", Utils.escapeHtml(input)] : undefined, + includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined, ]; const hash = params diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs index 664daef8..9f83b55c 100755 --- a/src/web/waiters/HighlighterWaiter.mjs +++ b/src/web/waiters/HighlighterWaiter.mjs @@ -155,12 +155,11 @@ class HighlighterWaiter { this.mouseTarget = INPUT; this.removeHighlights(); - const el = e.target; - const start = el.selectionStart; - const end = el.selectionEnd; + const sel = document.getSelection(); + const start = sel.baseOffset; + const end = sel.extentOffset; if (start !== 0 || end !== 0) { - document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end); this.highlightOutput([{start: start, end: end}]); } } @@ -248,12 +247,11 @@ class HighlighterWaiter { this.mouseTarget !== INPUT) return; - const el = e.target; - const start = el.selectionStart; - const end = el.selectionEnd; + const sel = document.getSelection(); + const start = sel.baseOffset; + const end = sel.extentOffset; if (start !== 0 || end !== 0) { - document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end); this.highlightOutput([{start: start, end: end}]); } } @@ -328,7 +326,6 @@ class HighlighterWaiter { removeHighlights() { document.getElementById("input-highlighter").innerHTML = ""; document.getElementById("output-highlighter").innerHTML = ""; - document.getElementById("input-selection-info").innerHTML = ""; document.getElementById("output-selection-info").innerHTML = ""; } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index b421d8d8..e8e71b12 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -7,9 +7,19 @@ import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js"; import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs"; -import Utils, { debounce } from "../../core/Utils.mjs"; -import { toBase64 } from "../../core/lib/Base64.mjs"; -import { isImage } from "../../core/lib/FileType.mjs"; +import Utils, {debounce} from "../../core/Utils.mjs"; +import {toBase64} from "../../core/lib/Base64.mjs"; +import {isImage} from "../../core/lib/FileType.mjs"; + +import { + EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor +} from "@codemirror/view"; +import {EditorState, Compartment} from "@codemirror/state"; +import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@codemirror/commands"; +import {bracketMatching} from "@codemirror/language"; +import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search"; + +import {statusBar} from "../extensions/statusBar.mjs"; /** @@ -27,6 +37,9 @@ class InputWaiter { this.app = app; this.manager = manager; + this.inputTextEl = document.getElementById("input-text"); + this.initEditor(); + // Define keys that don't change the input so we don't have to autobake when they are pressed this.badKeys = [ 16, // Shift @@ -61,6 +74,135 @@ class InputWaiter { } } + /** + * Sets up the CodeMirror Editor and returns the view + */ + initEditor() { + this.inputEditorConf = { + eol: new Compartment, + lineWrapping: new Compartment + }; + + const initialState = EditorState.create({ + doc: null, + extensions: [ + history(), + highlightSpecialChars({render: this.renderSpecialChar}), + drawSelection(), + rectangularSelection(), + crosshairCursor(), + bracketMatching(), + highlightSelectionMatches(), + search({top: true}), + statusBar(this.inputEditorConf), + this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping), + this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), + EditorState.allowMultipleSelections.of(true), + keymap.of([ + // Explicitly insert a tab rather than indenting the line + { key: "Tab", run: insertTab }, + // Explicitly insert a new line (using the current EOL char) rather + // than messing around with indenting, which does not respect EOL chars + { key: "Enter", run: insertNewline }, + ...historyKeymap, + ...defaultKeymap, + ...searchKeymap + ]), + ] + }); + + this.inputEditorView = new EditorView({ + state: initialState, + parent: this.inputTextEl + }); + } + + /** + * Override for rendering special characters. + * Should mirror the toDOM function in + * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150 + * But reverts the replacement of line feeds with newline control pictures. + * @param {number} code + * @param {string} desc + * @param {string} placeholder + * @returns {element} + */ + renderSpecialChar(code, desc, placeholder) { + const s = document.createElement("span"); + // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back. + s.textContent = code === 0x0a ? "\u240a" : placeholder; + s.title = desc; + s.setAttribute("aria-label", desc); + s.className = "cm-specialChar"; + return s; + } + + /** + * Handler for EOL Select clicks + * Sets the line separator + * @param {Event} e + */ + eolSelectClick(e) { + e.preventDefault(); + + const eolLookup = { + "LF": "\u000a", + "VT": "\u000b", + "FF": "\u000c", + "CR": "\u000d", + "CRLF": "\u000d\u000a", + "NEL": "\u0085", + "LS": "\u2028", + "PS": "\u2029" + }; + const eolval = eolLookup[e.target.getAttribute("data-val")]; + const oldInputVal = this.getInput(); + + // Update the EOL value + this.inputEditorView.dispatch({ + effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval)) + }); + + // Reset the input so that lines are recalculated, preserving the old EOL values + this.setInput(oldInputVal); + } + + /** + * Sets word wrap on the input editor + * @param {boolean} wrap + */ + setWordWrap(wrap) { + this.inputEditorView.dispatch({ + effects: this.inputEditorConf.lineWrapping.reconfigure( + wrap ? EditorView.lineWrapping : [] + ) + }); + } + + /** + * Gets the value of the current input + * @returns {string} + */ + getInput() { + const doc = this.inputEditorView.state.doc; + const eol = this.inputEditorView.state.lineBreak; + return doc.sliceString(0, doc.length, eol); + } + + /** + * Sets the value of the current input + * @param {string} data + */ + setInput(data) { + this.inputEditorView.dispatch({ + changes: { + from: 0, + to: this.inputEditorView.state.doc.length, + insert: data + } + }); + } + /** * Calculates the maximum number of tabs to display */ @@ -339,10 +481,8 @@ class InputWaiter { const activeTab = this.manager.tabs.getActiveInputTab(); if (inputData.inputNum !== activeTab) return; - const inputText = document.getElementById("input-text"); - if (typeof inputData.input === "string") { - inputText.value = inputData.input; + this.setInput(inputData.input); const fileOverlay = document.getElementById("input-file"), fileName = document.getElementById("input-file-name"), fileSize = document.getElementById("input-file-size"), @@ -355,17 +495,11 @@ class InputWaiter { fileType.textContent = ""; fileLoaded.textContent = ""; - inputText.style.overflow = "auto"; - inputText.classList.remove("blur"); - inputText.scroll(0, 0); - - const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ? - inputData.input.count("\n") + 1 : null; - this.setInputInfo(inputData.input.length, lines); + this.inputTextEl.classList.remove("blur"); // Set URL to current input const inputStr = toBase64(inputData.input, "A-Za-z0-9+/"); - if (inputStr.length > 0 && inputStr.length <= 68267) { + if (inputStr.length >= 0 && inputStr.length <= 68267) { this.setUrl({ includeInput: true, input: inputStr @@ -414,7 +548,6 @@ class InputWaiter { fileLoaded.textContent = inputData.progress + "%"; } - this.setInputInfo(inputData.size, null); this.displayFilePreview(inputData); if (!silent) window.dispatchEvent(this.manager.statechange); @@ -488,12 +621,10 @@ class InputWaiter { */ displayFilePreview(inputData) { const activeTab = this.manager.tabs.getActiveInputTab(), - input = inputData.input, - inputText = document.getElementById("input-text"); + input = inputData.input; if (inputData.inputNum !== activeTab) return; - inputText.style.overflow = "hidden"; - inputText.classList.add("blur"); - inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096))); + this.inputTextEl.classList.add("blur"); + this.setInput(Utils.arrayBufferToStr(input.slice(0, 4096))); this.renderFileThumb(); @@ -576,7 +707,7 @@ class InputWaiter { */ async getInputValue(inputNum) { return await new Promise(resolve => { - this.getInput(inputNum, false, r => { + this.getInputFromWorker(inputNum, false, r => { resolve(r.data); }); }); @@ -590,7 +721,7 @@ class InputWaiter { */ async getInputObj(inputNum) { return await new Promise(resolve => { - this.getInput(inputNum, true, r => { + this.getInputFromWorker(inputNum, true, r => { resolve(r.data); }); }); @@ -604,7 +735,7 @@ class InputWaiter { * @param {Function} callback - The callback to execute when the input is returned * @returns {ArrayBuffer | string | object} */ - getInput(inputNum, getObj, callback) { + getInputFromWorker(inputNum, getObj, callback) { const id = this.callbackID++; this.callbacks[id] = callback; @@ -647,29 +778,6 @@ class InputWaiter { }); } - - /** - * Displays information about the input. - * - * @param {number} length - The length of the current input string - * @param {number} lines - The number of the lines in the current input string - */ - setInputInfo(length, lines) { - let width = length.toString().length.toLocaleString(); - width = width < 2 ? 2 : width; - - const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " "); - let msg = "length: " + lengthStr; - - if (typeof lines === "number") { - const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " "); - msg += "
lines: " + linesStr; - } - - document.getElementById("input-info").innerHTML = msg; - - } - /** * Handler for input change events. * Debounces the input so we don't call autobake too often. @@ -696,17 +804,13 @@ class InputWaiter { // Remove highlighting from input and output panes as the offsets might be different now this.manager.highlighter.removeHighlights(); - const textArea = document.getElementById("input-text"); - const value = (textArea.value !== undefined) ? textArea.value : ""; + const value = this.getInput(); const activeTab = this.manager.tabs.getActiveInputTab(); this.app.progress = 0; - const lines = value.length < (this.app.options.ioDisplayThreshold * 1024) ? - (value.count("\n") + 1) : null; - this.setInputInfo(value.length, lines); this.updateInputValue(activeTab, value); - this.manager.tabs.updateInputTabHeader(activeTab, value.replace(/[\n\r]/g, "").slice(0, 100)); + this.manager.tabs.updateInputTabHeader(activeTab, value.slice(0, 100).replace(/[\n\r]/g, "")); if (e && this.badKeys.indexOf(e.keyCode) < 0) { // Fire the statechange event as the input has been modified @@ -714,62 +818,6 @@ class InputWaiter { } } - /** - * Handler for input paste events - * Checks that the size of the input is below the display limit, otherwise treats it as a file/blob - * - * @param {event} e - */ - async inputPaste(e) { - e.preventDefault(); - e.stopPropagation(); - - const self = this; - /** - * Triggers the input file/binary data overlay - * - * @param {string} pastedData - */ - function triggerOverlay(pastedData) { - const file = new File([pastedData], "PastedData", { - type: "text/plain", - lastModified: Date.now() - }); - - self.loadUIFiles([file]); - } - - const pastedData = e.clipboardData.getData("Text"); - const inputText = document.getElementById("input-text"); - const selStart = inputText.selectionStart; - const selEnd = inputText.selectionEnd; - const startVal = inputText.value.slice(0, selStart); - const endVal = inputText.value.slice(selEnd); - const val = startVal + pastedData + endVal; - - if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) { - // Data too large to display, use overlay - triggerOverlay(val); - return false; - } else if (await this.preserveCarriageReturns(val)) { - // Data contains a carriage return and the user doesn't wish to edit it, use overlay - // We check this in a separate condition to make sure it is not run unless absolutely - // necessary. - triggerOverlay(val); - return false; - } else { - // Pasting normally fires the inputChange() event before - // changing the value, so instead change it here ourselves - // and manually fire inputChange() - inputText.value = val; - inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length); - // Don't debounce here otherwise the keyup event for the Ctrl key will cancel an autobake - // (at least for large inputs) - this.inputChange(e, true); - } - } - - /** * Handler for input dragover events. * Gives the user a visual cue to show that items can be dropped here. @@ -818,7 +866,7 @@ class InputWaiter { if (text) { // Append the text to the current input and fire inputChange() - document.getElementById("input-text").value += text; + this.setInput(this.getInput() + text); this.inputChange(e); return; } @@ -843,44 +891,6 @@ class InputWaiter { } } - /** - * Checks if an input contains carriage returns. - * If a CR is detected, checks if the preserve CR option has been set, - * and if not, asks the user for their preference. - * - * @param {string} input - The input to be checked - * @returns {boolean} - If true, the input contains a CR which should be - * preserved, so display an overlay so it can't be edited - */ - async preserveCarriageReturns(input) { - if (input.indexOf("\r") < 0) return false; - - const optionsStr = "This behaviour can be changed in the
Options pane"; - const preserveStr = `A carriage return (\\r, 0x0d) was detected in your input. To preserve it, editing has been disabled.
${optionsStr}`; - const dontPreserveStr = `A carriage return (\\r, 0x0d) was detected in your input. It has not been preserved.
${optionsStr}`; - - switch (this.app.options.preserveCR) { - case "always": - this.app.alert(preserveStr, 6000); - return true; - case "never": - this.app.alert(dontPreserveStr, 6000); - return false; - } - - // Only preserve for high-entropy inputs - const data = Utils.strToArrayBuffer(input); - const entropy = Utils.calculateShannonEntropy(data); - - if (entropy > 6) { - this.app.alert(preserveStr, 6000); - return true; - } - - this.app.alert(dontPreserveStr, 6000); - return false; - } - /** * Load files from the UI into the inputWorker * @@ -1080,6 +1090,9 @@ class InputWaiter { this.manager.worker.setupChefWorker(); this.addInput(true); this.bakeAll(); + + // Fire the statechange event as the input has been modified + window.dispatchEvent(this.manager.statechange); } /** diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs index 5ef517d4..52b81ab4 100755 --- a/src/web/waiters/OptionsWaiter.mjs +++ b/src/web/waiters/OptionsWaiter.mjs @@ -53,6 +53,9 @@ class OptionsWaiter { selects[i].selectedIndex = 0; } } + + // Initialise options + this.setWordWrap(); } @@ -136,14 +139,13 @@ class OptionsWaiter { * Sets or unsets word wrap on the input and output depending on the wordWrap option value. */ setWordWrap() { - document.getElementById("input-text").classList.remove("word-wrap"); + this.manager.input.setWordWrap(this.app.options.wordWrap); document.getElementById("output-text").classList.remove("word-wrap"); document.getElementById("output-html").classList.remove("word-wrap"); document.getElementById("input-highlighter").classList.remove("word-wrap"); document.getElementById("output-highlighter").classList.remove("word-wrap"); if (!this.app.options.wordWrap) { - document.getElementById("input-text").classList.add("word-wrap"); document.getElementById("output-text").classList.add("word-wrap"); document.getElementById("output-html").classList.add("word-wrap"); document.getElementById("input-highlighter").classList.add("word-wrap"); diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 0eb6baec..8996edb0 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -1019,7 +1019,6 @@ class OutputWaiter { } document.getElementById("output-info").innerHTML = msg; - document.getElementById("input-selection-info").innerHTML = ""; document.getElementById("output-selection-info").innerHTML = ""; } @@ -1292,9 +1291,7 @@ class OutputWaiter { if (this.outputs[activeTab].data.type === "string" && active.byteLength <= this.app.options.ioDisplayThreshold * 1024) { const dishString = await this.getDishStr(this.getOutputDish(activeTab)); - if (!await this.manager.input.preserveCarriageReturns(dishString)) { - active = dishString; - } + active = dishString; } else { transferable.push(active); } diff --git a/tests/browser/nightwatch.js b/tests/browser/nightwatch.js index 41aff9b2..ba6f5204 100644 --- a/tests/browser/nightwatch.js +++ b/tests/browser/nightwatch.js @@ -82,7 +82,7 @@ module.exports = { // Enter input browser .useCss() - .setValue("#input-text", "Don't Panic.") + .setValue("#input-text", "Don't Panic.") // TODO .pause(1000) .click("#bake"); diff --git a/tests/browser/ops.js b/tests/browser/ops.js index bb18dc5d..d0933bb6 100644 --- a/tests/browser/ops.js +++ b/tests/browser/ops.js @@ -409,16 +409,16 @@ function bakeOp(browser, opName, input, args=[]) { .click("#clr-recipe") .click("#clr-io") .waitForElementNotPresent("#rec-list li.operation") - .expect.element("#input-text").to.have.property("value").that.equals(""); + .expect.element("#input-text").to.have.property("value").that.equals(""); // TODO browser .perform(function() { console.log(`Current test: ${opName}`); }) .urlHash("recipe=" + recipeConfig) - .setValue("#input-text", input) + .setValue("#input-text", input) // TODO .waitForElementPresent("#rec-list li.operation") - .expect.element("#input-text").to.have.property("value").that.equals(input); + .expect.element("#input-text").to.have.property("value").that.equals(input); // TODO browser .waitForElementVisible("#stale-indicator", 5000) From bc949b47d918fd77142c7fd22c086f5795d1a522 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 1 Jul 2022 12:01:48 +0100 Subject: [PATCH 163/686] Improved Controls CSS --- src/web/App.mjs | 1 + src/web/stylesheets/layout/_controls.css | 16 +++++----------- src/web/stylesheets/layout/_recipe.css | 1 - src/web/waiters/ControlsWaiter.mjs | 11 +++++++++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/web/App.mjs b/src/web/App.mjs index 9d4813e0..2d45d1f1 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -589,6 +589,7 @@ class App { this.manager.recipe.adjustWidth(); this.manager.input.calcMaxTabs(); this.manager.output.calcMaxTabs(); + this.manager.controls.calcControlsHeight(); } diff --git a/src/web/stylesheets/layout/_controls.css b/src/web/stylesheets/layout/_controls.css index c410704b..1edc41b5 100755 --- a/src/web/stylesheets/layout/_controls.css +++ b/src/web/stylesheets/layout/_controls.css @@ -6,27 +6,20 @@ * @license Apache-2.0 */ -:root { - --controls-height: 75px; -} - #controls { position: absolute; width: 100%; - height: var(--controls-height); bottom: 0; - padding: 0; - padding-top: 12px; + padding: 10px 0; border-top: 1px solid var(--primary-border-colour); background-color: var(--secondary-background-colour); } #controls-content { position: relative; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - transform-origin: center left; + display: flex; + flex-flow: row nowrap; + align-items: center; } #auto-bake-label { @@ -56,6 +49,7 @@ #controls .btn { border-radius: 30px; + margin: 0; } .output-maximised .hide-on-maximised-output { diff --git a/src/web/stylesheets/layout/_recipe.css b/src/web/stylesheets/layout/_recipe.css index bd70d10f..339da074 100755 --- a/src/web/stylesheets/layout/_recipe.css +++ b/src/web/stylesheets/layout/_recipe.css @@ -7,7 +7,6 @@ */ #rec-list { - bottom: var(--controls-height); overflow: auto; } diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 426107bb..2879089a 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -410,6 +410,17 @@ ${navigator.userAgent} } } + /** + * Calculates the height of the controls area and adjusts the recipe + * height accordingly. + */ + calcControlsHeight() { + const controls = document.getElementById("controls"), + recList = document.getElementById("rec-list"); + + recList.style.bottom = controls.clientHeight + "px"; + } + } export default ControlsWaiter; From 68733c74cc5dd5067d750c28e32708e9e7a280a0 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sat, 2 Jul 2022 19:23:03 +0100 Subject: [PATCH 164/686] Output now uses CodeMirror editor --- src/core/Utils.mjs | 2 +- src/web/Manager.mjs | 4 - src/web/extensions/statusBar.mjs | 190 ----------------- src/web/html/index.html | 7 +- src/web/stylesheets/layout/_io.css | 48 +---- src/web/utils/editorUtils.mjs | 28 +++ src/web/utils/htmlWidget.mjs | 87 ++++++++ src/web/utils/statusBar.mjs | 271 ++++++++++++++++++++++++ src/web/waiters/HighlighterWaiter.mjs | 173 ++++++---------- src/web/waiters/InputWaiter.mjs | 48 +---- src/web/waiters/OptionsWaiter.mjs | 5 +- src/web/waiters/OutputWaiter.mjs | 285 +++++++++++++++++--------- tests/browser/nightwatch.js | 4 +- tests/browser/ops.js | 8 +- 14 files changed, 665 insertions(+), 495 deletions(-) delete mode 100644 src/web/extensions/statusBar.mjs create mode 100644 src/web/utils/editorUtils.mjs create mode 100644 src/web/utils/htmlWidget.mjs create mode 100644 src/web/utils/statusBar.mjs diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 66a98c36..5f36cae9 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -424,7 +424,7 @@ class Utils { const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { - if (isWorkerEnvironment()) { + if (isWorkerEnvironment() && self && typeof self.setOption === "function") { self.setOption("attemptHighlight", false); } else if (isWebEnvironment()) { window.app.options.attemptHighlight = false; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 08a35d75..2477bb60 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -178,7 +178,6 @@ class Manager { this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input); document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input)); document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input)); - this.addDynamicListener(".eol-select a", "click", this.input.eolSelectClick, this.input); // Output @@ -192,10 +191,7 @@ class Manager { document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter)); document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter)); document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter)); - document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter)); - document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter)); this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter); - this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter); this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); this.addDynamicListener("#output-file-show-all", "click", this.output.showAllFile, this.output); this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output); diff --git a/src/web/extensions/statusBar.mjs b/src/web/extensions/statusBar.mjs deleted file mode 100644 index 8a837a51..00000000 --- a/src/web/extensions/statusBar.mjs +++ /dev/null @@ -1,190 +0,0 @@ -/** - * A Status bar extension for CodeMirror - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2022 - * @license Apache-2.0 - */ - -import {showPanel} from "@codemirror/view"; - -/** - * Counts the stats of a document - * @param {element} el - * @param {Text} doc - */ -function updateStats(el, doc) { - const length = el.querySelector("#stats-length-value"), - lines = el.querySelector("#stats-lines-value"); - length.textContent = doc.length; - lines.textContent = doc.lines; -} - -/** - * Gets the current selection info - * @param {element} el - * @param {EditorState} state - * @param {boolean} selectionSet - */ -function updateSelection(el, state, selectionSet) { - const selLen = state.selection && state.selection.main ? - state.selection.main.to - state.selection.main.from : - 0; - - const selInfo = el.querySelector("#sel-info"), - curOffsetInfo = el.querySelector("#cur-offset-info"); - - if (!selectionSet) { - selInfo.style.display = "none"; - curOffsetInfo.style.display = "none"; - return; - } - - if (selLen > 0) { // Range - const start = el.querySelector("#sel-start-value"), - end = el.querySelector("#sel-end-value"), - length = el.querySelector("#sel-length-value"); - - selInfo.style.display = "inline-block"; - curOffsetInfo.style.display = "none"; - - start.textContent = state.selection.main.from; - end.textContent = state.selection.main.to; - length.textContent = state.selection.main.to - state.selection.main.from; - } else { // Position - const offset = el.querySelector("#cur-offset-value"); - - selInfo.style.display = "none"; - curOffsetInfo.style.display = "inline-block"; - - offset.textContent = state.selection.main.from; - } -} - -/** - * Gets the current character encoding of the document - * @param {element} el - * @param {EditorState} state - */ -function updateCharEnc(el, state) { - // const charenc = el.querySelector("#char-enc-value"); - // TODO - // charenc.textContent = "TODO"; -} - -/** - * Returns what the current EOL separator is set to - * @param {element} el - * @param {EditorState} state - */ -function updateEOL(el, state) { - const eolLookup = { - "\u000a": "LF", - "\u000b": "VT", - "\u000c": "FF", - "\u000d": "CR", - "\u000d\u000a": "CRLF", - "\u0085": "NEL", - "\u2028": "LS", - "\u2029": "PS" - }; - - const val = el.querySelector("#eol-value"); - val.textContent = eolLookup[state.lineBreak]; -} - -/** - * Builds the Left-hand-side widgets - * @returns {string} - */ -function constructLHS() { - return ` - abc - - - - sort - - - - - highlight_alt - \u279E - ( selected) - - - location_on - - `; -} - -/** - * Builds the Right-hand-side widgets - * Event listener set up in Manager - * @returns {string} - */ -function constructRHS() { - return ` - language - UTF-16 - - - `; -} - -/** - * A panel constructor building a panel that re-counts the stats every time the document changes. - * @param {EditorView} view - * @returns {Panel} - */ -function wordCountPanel(view) { - const dom = document.createElement("div"); - const lhs = document.createElement("div"); - const rhs = document.createElement("div"); - - dom.className = "cm-status-bar"; - lhs.innerHTML = constructLHS(); - rhs.innerHTML = constructRHS(); - - dom.appendChild(lhs); - dom.appendChild(rhs); - - updateEOL(rhs, view.state); - updateCharEnc(rhs, view.state); - updateStats(lhs, view.state.doc); - updateSelection(lhs, view.state, false); - - return { - dom, - update(update) { - updateEOL(rhs, update.state); - updateSelection(lhs, update.state, update.selectionSet); - updateCharEnc(rhs, update.state); - if (update.docChanged) { - updateStats(lhs, update.state.doc); - } - } - }; -} - -/** - * A function that build the extension that enables the panel in an editor. - * @returns {Extension} - */ -export function statusBar() { - return showPanel.of(wordCountPanel); -} diff --git a/src/web/html/index.html b/src/web/html/index.html index 3d237bdd..3eb150e5 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -191,7 +191,7 @@
    -
    +
    @@ -289,8 +289,6 @@
    -
    -
    @@ -344,8 +342,7 @@
    -
    - +
    diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 625b81f7..ba670f3d 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -6,7 +6,8 @@ * @license Apache-2.0 */ -#input-text { +#input-text, +#output-text { position: relative; width: 100%; height: 100%; @@ -24,23 +25,6 @@ color: var(--fixed-width-font-colour); } -#output-text, -#output-html { - position: relative; - width: 100%; - height: 100%; - margin: 0; - padding: 3px; - -moz-padding-start: 3px; - -moz-padding-end: 3px; - border: none; - border-width: 0px; - resize: none; - background-color: transparent; - white-space: pre-wrap; - word-wrap: break-word; -} - #output-wrapper{ margin: 0; padding: 0; @@ -54,13 +38,6 @@ pointer-events: auto; } - -#output-html { - display: none; - overflow-y: auto; - -moz-padding-start: 1px; /* Fixes bug in Firefox */ -} - #input-tabs-wrapper #input-tabs, #output-tabs-wrapper #output-tabs { list-style: none; @@ -179,25 +156,15 @@ } #input-wrapper, -#output-wrapper, -#input-wrapper > :not(#input-text), -#output-wrapper > .textarea-wrapper > div, -#output-wrapper > .textarea-wrapper > textarea { +#output-wrapper { height: calc(100% - var(--title-height)); } #input-wrapper.show-tabs, -#input-wrapper.show-tabs > :not(#input-text), -#output-wrapper.show-tabs, -#output-wrapper.show-tabs > .textarea-wrapper > div, -#output-wrapper.show-tabs > .textarea-wrapper > textarea { +#output-wrapper.show-tabs { height: calc(100% - var(--tab-height) - var(--title-height)); } -#output-wrapper > .textarea-wrapper > #output-html { - height: 100%; -} - #show-file-overlay { height: 32px; } @@ -211,7 +178,6 @@ .textarea-wrapper textarea, .textarea-wrapper #output-text, -.textarea-wrapper #output-html, .textarea-wrapper #output-highlighter { font-family: var(--fixed-width-font-family); font-size: var(--fixed-width-font-size); @@ -477,6 +443,12 @@ /* Status bar */ +.ͼ2 .cm-panels { + background-color: var(--secondary-background-colour); + border-color: var(--secondary-border-colour); + color: var(--primary-font-colour); +} + .cm-status-bar { font-family: var(--fixed-width-font-family); font-weight: normal; diff --git a/src/web/utils/editorUtils.mjs b/src/web/utils/editorUtils.mjs new file mode 100644 index 00000000..fe6b83d4 --- /dev/null +++ b/src/web/utils/editorUtils.mjs @@ -0,0 +1,28 @@ +/** + * CodeMirror utilities that are relevant to both the input and output + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + + +/** + * Override for rendering special characters. + * Should mirror the toDOM function in + * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150 + * But reverts the replacement of line feeds with newline control pictures. + * @param {number} code + * @param {string} desc + * @param {string} placeholder + * @returns {element} + */ +export function renderSpecialChar(code, desc, placeholder) { + const s = document.createElement("span"); + // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back. + s.textContent = code === 0x0a ? "\u240a" : placeholder; + s.title = desc; + s.setAttribute("aria-label", desc); + s.className = "cm-specialChar"; + return s; +} diff --git a/src/web/utils/htmlWidget.mjs b/src/web/utils/htmlWidget.mjs new file mode 100644 index 00000000..fbce9b49 --- /dev/null +++ b/src/web/utils/htmlWidget.mjs @@ -0,0 +1,87 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import {WidgetType, Decoration, ViewPlugin} from "@codemirror/view"; + +/** + * Adds an HTML widget to the Code Mirror editor + */ +class HTMLWidget extends WidgetType { + + /** + * HTMLWidget consructor + */ + constructor(html) { + super(); + this.html = html; + } + + /** + * Builds the DOM node + * @returns {DOMNode} + */ + toDOM() { + const wrap = document.createElement("span"); + wrap.setAttribute("id", "output-html"); + wrap.innerHTML = this.html; + return wrap; + } + +} + +/** + * Decorator function to provide a set of widgets for the editor DOM + * @param {EditorView} view + * @param {string} html + * @returns {DecorationSet} + */ +function decorateHTML(view, html) { + const widgets = []; + if (html.length) { + const deco = Decoration.widget({ + widget: new HTMLWidget(html), + side: 1 + }); + widgets.push(deco.range(0)); + } + return Decoration.set(widgets); +} + + +/** + * An HTML Plugin builder + * @param {Object} htmlOutput + * @returns {ViewPlugin} + */ +export function htmlPlugin(htmlOutput) { + const plugin = ViewPlugin.fromClass( + class { + /** + * Plugin constructor + * @param {EditorView} view + */ + constructor(view) { + this.htmlOutput = htmlOutput; + this.decorations = decorateHTML(view, this.htmlOutput.html); + } + + /** + * Editor update listener + * @param {ViewUpdate} update + */ + update(update) { + if (this.htmlOutput.changed) { + this.decorations = decorateHTML(update.view, this.htmlOutput.html); + this.htmlOutput.changed = false; + } + } + }, { + decorations: v => v.decorations + } + ); + + return plugin; +} diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs new file mode 100644 index 00000000..431d8a3d --- /dev/null +++ b/src/web/utils/statusBar.mjs @@ -0,0 +1,271 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import {showPanel} from "@codemirror/view"; + +/** + * A Status bar extension for CodeMirror + */ +class StatusBarPanel { + + /** + * StatusBarPanel constructor + * @param {Object} opts + */ + constructor(opts) { + this.label = opts.label; + this.bakeStats = opts.bakeStats ? opts.bakeStats : null; + this.eolHandler = opts.eolHandler; + + this.dom = this.buildDOM(); + } + + /** + * Builds the status bar DOM tree + * @returns {DOMNode} + */ + buildDOM() { + const dom = document.createElement("div"); + const lhs = document.createElement("div"); + const rhs = document.createElement("div"); + + dom.className = "cm-status-bar"; + lhs.innerHTML = this.constructLHS(); + rhs.innerHTML = this.constructRHS(); + + dom.appendChild(lhs); + dom.appendChild(rhs); + + // Event listeners + dom.addEventListener("click", this.eolSelectClick.bind(this), false); + + return dom; + } + + /** + * Handler for EOL Select clicks + * Sets the line separator + * @param {Event} e + */ + eolSelectClick(e) { + e.preventDefault(); + + const eolLookup = { + "LF": "\u000a", + "VT": "\u000b", + "FF": "\u000c", + "CR": "\u000d", + "CRLF": "\u000d\u000a", + "NEL": "\u0085", + "LS": "\u2028", + "PS": "\u2029" + }; + const eolval = eolLookup[e.target.getAttribute("data-val")]; + + // Call relevant EOL change handler + this.eolHandler(eolval); + } + + /** + * Counts the stats of a document + * @param {Text} doc + */ + updateStats(doc) { + const length = this.dom.querySelector(".stats-length-value"), + lines = this.dom.querySelector(".stats-lines-value"); + length.textContent = doc.length; + lines.textContent = doc.lines; + } + + /** + * Gets the current selection info + * @param {EditorState} state + * @param {boolean} selectionSet + */ + updateSelection(state, selectionSet) { + const selLen = state.selection && state.selection.main ? + state.selection.main.to - state.selection.main.from : + 0; + + const selInfo = this.dom.querySelector(".sel-info"), + curOffsetInfo = this.dom.querySelector(".cur-offset-info"); + + if (!selectionSet) { + selInfo.style.display = "none"; + curOffsetInfo.style.display = "none"; + return; + } + + if (selLen > 0) { // Range + const start = this.dom.querySelector(".sel-start-value"), + end = this.dom.querySelector(".sel-end-value"), + length = this.dom.querySelector(".sel-length-value"); + + selInfo.style.display = "inline-block"; + curOffsetInfo.style.display = "none"; + + start.textContent = state.selection.main.from; + end.textContent = state.selection.main.to; + length.textContent = state.selection.main.to - state.selection.main.from; + } else { // Position + const offset = this.dom.querySelector(".cur-offset-value"); + + selInfo.style.display = "none"; + curOffsetInfo.style.display = "inline-block"; + + offset.textContent = state.selection.main.from; + } + } + + /** + * Gets the current character encoding of the document + * @param {EditorState} state + */ + updateCharEnc(state) { + // const charenc = this.dom.querySelector("#char-enc-value"); + // TODO + // charenc.textContent = "TODO"; + } + + /** + * Returns what the current EOL separator is set to + * @param {EditorState} state + */ + updateEOL(state) { + const eolLookup = { + "\u000a": "LF", + "\u000b": "VT", + "\u000c": "FF", + "\u000d": "CR", + "\u000d\u000a": "CRLF", + "\u0085": "NEL", + "\u2028": "LS", + "\u2029": "PS" + }; + + const val = this.dom.querySelector(".eol-value"); + val.textContent = eolLookup[state.lineBreak]; + } + + /** + * Sets the latest bake duration + */ + updateBakeStats() { + const bakingTime = this.dom.querySelector(".baking-time-value"); + const bakingTimeInfo = this.dom.querySelector(".baking-time-info"); + + if (this.label === "Output" && + this.bakeStats && + typeof this.bakeStats.duration === "number" && + this.bakeStats.duration >= 0) { + bakingTimeInfo.style.display = "inline-block"; + bakingTime.textContent = this.bakeStats.duration; + } else { + bakingTimeInfo.style.display = "none"; + } + } + + /** + * Builds the Left-hand-side widgets + * @returns {string} + */ + constructLHS() { + return ` + + abc + + + + sort + + + + + highlight_alt + \u279E + ( selected) + + + location_on + + `; + } + + /** + * Builds the Right-hand-side widgets + * Event listener set up in Manager + * @returns {string} + */ + constructRHS() { + return ` + + + + language + UTF-16 + + + `; + } + +} + +/** + * A panel constructor factory building a panel that re-counts the stats every time the document changes. + * @param {Object} opts + * @returns {Function} + */ +function makePanel(opts) { + const sbPanel = new StatusBarPanel(opts); + + return (view) => { + sbPanel.updateEOL(view.state); + sbPanel.updateCharEnc(view.state); + sbPanel.updateBakeStats(); + sbPanel.updateStats(view.state.doc); + sbPanel.updateSelection(view.state, false); + + return { + "dom": sbPanel.dom, + update(update) { + sbPanel.updateEOL(update.state); + sbPanel.updateSelection(update.state, update.selectionSet); + sbPanel.updateCharEnc(update.state); + sbPanel.updateBakeStats(); + if (update.docChanged) { + sbPanel.updateStats(update.state.doc); + } + } + }; + }; +} + +/** + * A function that build the extension that enables the panel in an editor. + * @param {Object} opts + * @returns {Extension} + */ +export function statusBar(opts) { + const panelMaker = makePanel(opts); + return showPanel.of(panelMaker); +} diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs index 9f83b55c..d1340165 100755 --- a/src/web/waiters/HighlighterWaiter.mjs +++ b/src/web/waiters/HighlighterWaiter.mjs @@ -176,34 +176,16 @@ class HighlighterWaiter { this.mouseTarget = OUTPUT; this.removeHighlights(); - const el = e.target; - const start = el.selectionStart; - const end = el.selectionEnd; + const sel = document.getSelection(); + const start = sel.baseOffset; + const end = sel.extentOffset; if (start !== 0 || end !== 0) { - document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end); this.highlightInput([{start: start, end: end}]); } } - /** - * Handler for output HTML mousedown events. - * Calculates the current selection info. - * - * @param {event} e - */ - outputHtmlMousedown(e) { - this.mouseButtonDown = true; - this.mouseTarget = OUTPUT; - - const sel = this._getOutputHtmlSelectionOffsets(); - if (sel.start !== 0 || sel.end !== 0) { - document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end); - } - } - - /** * Handler for input mouseup events. * @@ -224,16 +206,6 @@ class HighlighterWaiter { } - /** - * Handler for output HTML mouseup events. - * - * @param {event} e - */ - outputHtmlMouseup(e) { - this.mouseButtonDown = false; - } - - /** * Handler for input mousemove events. * Calculates the current selection info, and highlights the corresponding data in the output. @@ -270,37 +242,16 @@ class HighlighterWaiter { this.mouseTarget !== OUTPUT) return; - const el = e.target; - const start = el.selectionStart; - const end = el.selectionEnd; + const sel = document.getSelection(); + const start = sel.baseOffset; + const end = sel.extentOffset; if (start !== 0 || end !== 0) { - document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end); this.highlightInput([{start: start, end: end}]); } } - /** - * Handler for output HTML mousemove events. - * Calculates the current selection info. - * - * @param {event} e - */ - outputHtmlMousemove(e) { - // Check that the left mouse button is pressed - if (!this.mouseButtonDown || - e.which !== 1 || - this.mouseTarget !== OUTPUT) - return; - - const sel = this._getOutputHtmlSelectionOffsets(); - if (sel.start !== 0 || sel.end !== 0) { - document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end); - } - } - - /** * Given start and end offsets, writes the HTML for the selection info element with the correct * padding. @@ -326,7 +277,6 @@ class HighlighterWaiter { removeHighlights() { document.getElementById("input-highlighter").innerHTML = ""; document.getElementById("output-highlighter").innerHTML = ""; - document.getElementById("output-selection-info").innerHTML = ""; } @@ -379,7 +329,8 @@ class HighlighterWaiter { const io = direction === "forward" ? "output" : "input"; - document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end); + // TODO + // document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end); this.highlight( document.getElementById(io + "-text"), document.getElementById(io + "-highlighter"), @@ -398,67 +349,67 @@ class HighlighterWaiter { * @param {number} pos.end - The end offset. */ async highlight(textarea, highlighter, pos) { - if (!this.app.options.showHighlighter) return false; - if (!this.app.options.attemptHighlight) return false; + // if (!this.app.options.showHighlighter) return false; + // if (!this.app.options.attemptHighlight) return false; - // Check if there is a carriage return in the output dish as this will not - // be displayed by the HTML textarea and will mess up highlighting offsets. - if (await this.manager.output.containsCR()) return false; + // // Check if there is a carriage return in the output dish as this will not + // // be displayed by the HTML textarea and will mess up highlighting offsets. + // if (await this.manager.output.containsCR()) return false; - const startPlaceholder = "[startHighlight]"; - const startPlaceholderRegex = /\[startHighlight\]/g; - const endPlaceholder = "[endHighlight]"; - const endPlaceholderRegex = /\[endHighlight\]/g; - let text = textarea.value; + // const startPlaceholder = "[startHighlight]"; + // const startPlaceholderRegex = /\[startHighlight\]/g; + // const endPlaceholder = "[endHighlight]"; + // const endPlaceholderRegex = /\[endHighlight\]/g; + // // let text = textarea.value; // TODO - // Put placeholders in position - // If there's only one value, select that - // If there are multiple, ignore the first one and select all others - if (pos.length === 1) { - if (pos[0].end < pos[0].start) return; - text = text.slice(0, pos[0].start) + - startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder + - text.slice(pos[0].end, text.length); - } else { - // O(n^2) - Can anyone improve this without overwriting placeholders? - let result = "", - endPlaced = true; + // // Put placeholders in position + // // If there's only one value, select that + // // If there are multiple, ignore the first one and select all others + // if (pos.length === 1) { + // if (pos[0].end < pos[0].start) return; + // text = text.slice(0, pos[0].start) + + // startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder + + // text.slice(pos[0].end, text.length); + // } else { + // // O(n^2) - Can anyone improve this without overwriting placeholders? + // let result = "", + // endPlaced = true; - for (let i = 0; i < text.length; i++) { - for (let j = 1; j < pos.length; j++) { - if (pos[j].end < pos[j].start) continue; - if (pos[j].start === i) { - result += startPlaceholder; - endPlaced = false; - } - if (pos[j].end === i) { - result += endPlaceholder; - endPlaced = true; - } - } - result += text[i]; - } - if (!endPlaced) result += endPlaceholder; - text = result; - } + // for (let i = 0; i < text.length; i++) { + // for (let j = 1; j < pos.length; j++) { + // if (pos[j].end < pos[j].start) continue; + // if (pos[j].start === i) { + // result += startPlaceholder; + // endPlaced = false; + // } + // if (pos[j].end === i) { + // result += endPlaceholder; + // endPlaced = true; + // } + // } + // result += text[i]; + // } + // if (!endPlaced) result += endPlaceholder; + // text = result; + // } - const cssClass = "hl1"; + // const cssClass = "hl1"; - // Remove HTML tags - text = text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/\n/g, " ") - // Convert placeholders to tags - .replace(startPlaceholderRegex, "") - .replace(endPlaceholderRegex, "") + " "; + // // Remove HTML tags + // text = text + // .replace(/&/g, "&") + // .replace(//g, ">") + // .replace(/\n/g, " ") + // // Convert placeholders to tags + // .replace(startPlaceholderRegex, "") + // .replace(endPlaceholderRegex, "") + " "; - // Adjust width to allow for scrollbars - highlighter.style.width = textarea.clientWidth + "px"; - highlighter.innerHTML = text; - highlighter.scrollTop = textarea.scrollTop; - highlighter.scrollLeft = textarea.scrollLeft; + // // Adjust width to allow for scrollbars + // highlighter.style.width = textarea.clientWidth + "px"; + // highlighter.innerHTML = text; + // highlighter.scrollTop = textarea.scrollTop; + // highlighter.scrollLeft = textarea.scrollLeft; } } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index e8e71b12..0dc44dbe 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -19,7 +19,8 @@ import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@ import {bracketMatching} from "@codemirror/language"; import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search"; -import {statusBar} from "../extensions/statusBar.mjs"; +import {statusBar} from "../utils/statusBar.mjs"; +import {renderSpecialChar} from "../utils/editorUtils.mjs"; /** @@ -87,14 +88,17 @@ class InputWaiter { doc: null, extensions: [ history(), - highlightSpecialChars({render: this.renderSpecialChar}), + highlightSpecialChars({render: renderSpecialChar}), drawSelection(), rectangularSelection(), crosshairCursor(), bracketMatching(), highlightSelectionMatches(), search({top: true}), - statusBar(this.inputEditorConf), + statusBar({ + label: "Input", + eolHandler: this.eolChange.bind(this) + }), this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping), this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), EditorState.allowMultipleSelections.of(true), @@ -118,44 +122,10 @@ class InputWaiter { } /** - * Override for rendering special characters. - * Should mirror the toDOM function in - * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150 - * But reverts the replacement of line feeds with newline control pictures. - * @param {number} code - * @param {string} desc - * @param {string} placeholder - * @returns {element} - */ - renderSpecialChar(code, desc, placeholder) { - const s = document.createElement("span"); - // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back. - s.textContent = code === 0x0a ? "\u240a" : placeholder; - s.title = desc; - s.setAttribute("aria-label", desc); - s.className = "cm-specialChar"; - return s; - } - - /** - * Handler for EOL Select clicks + * Handler for EOL change events * Sets the line separator - * @param {Event} e */ - eolSelectClick(e) { - e.preventDefault(); - - const eolLookup = { - "LF": "\u000a", - "VT": "\u000b", - "FF": "\u000c", - "CR": "\u000d", - "CRLF": "\u000d\u000a", - "NEL": "\u0085", - "LS": "\u2028", - "PS": "\u2029" - }; - const eolval = eolLookup[e.target.getAttribute("data-val")]; + eolChange(eolval) { const oldInputVal = this.getInput(); // Update the EOL value diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs index 52b81ab4..7d9a3e2d 100755 --- a/src/web/waiters/OptionsWaiter.mjs +++ b/src/web/waiters/OptionsWaiter.mjs @@ -140,14 +140,11 @@ class OptionsWaiter { */ setWordWrap() { this.manager.input.setWordWrap(this.app.options.wordWrap); - document.getElementById("output-text").classList.remove("word-wrap"); - document.getElementById("output-html").classList.remove("word-wrap"); + this.manager.output.setWordWrap(this.app.options.wordWrap); document.getElementById("input-highlighter").classList.remove("word-wrap"); document.getElementById("output-highlighter").classList.remove("word-wrap"); if (!this.app.options.wordWrap) { - document.getElementById("output-text").classList.add("word-wrap"); - document.getElementById("output-html").classList.add("word-wrap"); document.getElementById("input-highlighter").classList.add("word-wrap"); document.getElementById("output-highlighter").classList.add("word-wrap"); } diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 8996edb0..496b0ac5 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -10,6 +10,18 @@ import Dish from "../../core/Dish.mjs"; import FileSaver from "file-saver"; import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs"; +import { + EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor +} from "@codemirror/view"; +import {EditorState, Compartment} from "@codemirror/state"; +import {defaultKeymap} from "@codemirror/commands"; +import {bracketMatching} from "@codemirror/language"; +import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search"; + +import {statusBar} from "../utils/statusBar.mjs"; +import {renderSpecialChar} from "../utils/editorUtils.mjs"; +import {htmlPlugin} from "../utils/htmlWidget.mjs"; + /** * Waiter to handle events related to the output */ @@ -25,12 +37,155 @@ class OutputWaiter { this.app = app; this.manager = manager; + this.outputTextEl = document.getElementById("output-text"); + // Object to contain bake statistics - used by statusBar extension + this.bakeStats = { + duration: 0 + }; + // Object to handle output HTML state - used by htmlWidget extension + this.htmlOutput = { + html: "", + changed: false + }; + this.initEditor(); + this.outputs = {}; this.zipWorker = null; this.maxTabs = this.manager.tabs.calcMaxTabs(); this.tabTimeout = null; } + /** + * Sets up the CodeMirror Editor and returns the view + */ + initEditor() { + this.outputEditorConf = { + eol: new Compartment, + lineWrapping: new Compartment + }; + + const initialState = EditorState.create({ + doc: null, + extensions: [ + EditorState.readOnly.of(true), + htmlPlugin(this.htmlOutput), + highlightSpecialChars({render: renderSpecialChar}), + drawSelection(), + rectangularSelection(), + crosshairCursor(), + bracketMatching(), + highlightSelectionMatches(), + search({top: true}), + statusBar({ + label: "Output", + bakeStats: this.bakeStats, + eolHandler: this.eolChange.bind(this) + }), + this.outputEditorConf.lineWrapping.of(EditorView.lineWrapping), + this.outputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), + EditorState.allowMultipleSelections.of(true), + keymap.of([ + ...defaultKeymap, + ...searchKeymap + ]), + ] + }); + + this.outputEditorView = new EditorView({ + state: initialState, + parent: this.outputTextEl + }); + } + + /** + * Handler for EOL change events + * Sets the line separator + */ + eolChange(eolval) { + const oldOutputVal = this.getOutput(); + + // Update the EOL value + this.outputEditorView.dispatch({ + effects: this.outputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval)) + }); + + // Reset the output so that lines are recalculated, preserving the old EOL values + this.setOutput(oldOutputVal); + } + + /** + * Sets word wrap on the output editor + * @param {boolean} wrap + */ + setWordWrap(wrap) { + this.outputEditorView.dispatch({ + effects: this.outputEditorConf.lineWrapping.reconfigure( + wrap ? EditorView.lineWrapping : [] + ) + }); + } + + /** + * Gets the value of the current output + * @returns {string} + */ + getOutput() { + const doc = this.outputEditorView.state.doc; + const eol = this.outputEditorView.state.lineBreak; + return doc.sliceString(0, doc.length, eol); + } + + /** + * Sets the value of the current output + * @param {string} data + */ + setOutput(data) { + this.outputEditorView.dispatch({ + changes: { + from: 0, + to: this.outputEditorView.state.doc.length, + insert: data + } + }); + } + + /** + * Sets the value of the current output to a rendered HTML value + * @param {string} html + */ + setHTMLOutput(html) { + this.htmlOutput.html = html; + this.htmlOutput.changed = true; + // This clears the text output, but also fires a View update which + // triggers the htmlWidget to render the HTML. + this.setOutput(""); + + // Execute script sections + const scriptElements = document.getElementById("output-html").querySelectorAll("script"); + for (let i = 0; i < scriptElements.length; i++) { + try { + eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval + } catch (err) { + log.error(err); + } + } + } + + /** + * Clears the HTML output + */ + clearHTMLOutput() { + this.htmlOutput.html = ""; + this.htmlOutput.changed = true; + // Fire a blank change to force the htmlWidget to update and remove any HTML + this.outputEditorView.dispatch({ + changes: { + from: 0, + insert: "" + } + }); + } + /** * Calculates the maximum number of tabs to display */ @@ -245,8 +400,6 @@ class OutputWaiter { activeTab = this.manager.tabs.getActiveOutputTab(); if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10); - const outputText = document.getElementById("output-text"); - const outputHtml = document.getElementById("output-html"); const outputFile = document.getElementById("output-file"); const outputHighlighter = document.getElementById("output-highlighter"); const inputHighlighter = document.getElementById("input-highlighter"); @@ -278,95 +431,68 @@ class OutputWaiter { } else if (output.status === "error") { // style the tab if it's being shown this.toggleLoader(false); - outputText.style.display = "block"; - outputText.classList.remove("blur"); - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; + this.outputTextEl.classList.remove("blur"); outputFile.style.display = "none"; outputHighlighter.display = "none"; inputHighlighter.display = "none"; + this.clearHTMLOutput(); if (output.error) { - outputText.value = output.error; + this.setOutput(output.error); } else { - outputText.value = output.data.result; + this.setOutput(output.data.result); } - outputHtml.innerHTML = ""; } else if (output.status === "baked" || output.status === "inactive") { document.querySelector("#output-loader .loading-msg").textContent = `Loading output ${inputNum}`; this.closeFile(); - let scriptElements, lines, length; if (output.data === null) { - outputText.style.display = "block"; - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; outputHighlighter.display = "block"; inputHighlighter.display = "block"; - outputText.value = ""; - outputHtml.innerHTML = ""; + this.clearHTMLOutput(); + this.setOutput(""); this.toggleLoader(false); return; } + this.bakeStats.duration = output.data.duration; + switch (output.data.type) { case "html": - outputText.style.display = "none"; - outputHtml.style.display = "block"; outputFile.style.display = "none"; outputHighlighter.style.display = "none"; inputHighlighter.style.display = "none"; - outputText.value = ""; - outputHtml.innerHTML = output.data.result; - - // Execute script sections - scriptElements = outputHtml.querySelectorAll("script"); - for (let i = 0; i < scriptElements.length; i++) { - try { - eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval - } catch (err) { - log.error(err); - } - } + this.setHTMLOutput(output.data.result); break; case "ArrayBuffer": - outputText.style.display = "block"; - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; outputHighlighter.display = "none"; inputHighlighter.display = "none"; - outputText.value = ""; - outputHtml.innerHTML = ""; + this.clearHTMLOutput(); + this.setOutput(""); - length = output.data.result.byteLength; this.setFile(await this.getDishBuffer(output.data.dish), activeTab); break; case "string": default: - outputText.style.display = "block"; - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; outputHighlighter.display = "block"; inputHighlighter.display = "block"; - outputText.value = Utils.printable(output.data.result, true); - outputHtml.innerHTML = ""; - - lines = output.data.result.count("\n") + 1; - length = output.data.result.length; + this.clearHTMLOutput(); + this.setOutput(output.data.result); break; } this.toggleLoader(false); - if (output.data.type === "html") { - const dishStr = await this.getDishStr(output.data.dish); - length = dishStr.length; - lines = dishStr.count("\n") + 1; - } - - this.setOutputInfo(length, lines, output.data.duration); debounce(this.backgroundMagic, 50, "backgroundMagic", this, [])(); } }.bind(this)); @@ -383,14 +509,13 @@ class OutputWaiter { // Display file overlay in output area with details const fileOverlay = document.getElementById("output-file"), fileSize = document.getElementById("output-file-size"), - outputText = document.getElementById("output-text"), fileSlice = buf.slice(0, 4096); fileOverlay.style.display = "block"; fileSize.textContent = buf.byteLength.toLocaleString() + " bytes"; - outputText.classList.add("blur"); - outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice)); + this.outputTextEl.classList.add("blur"); + this.setOutput(Utils.arrayBufferToStr(fileSlice)); } /** @@ -398,7 +523,7 @@ class OutputWaiter { */ closeFile() { document.getElementById("output-file").style.display = "none"; - document.getElementById("output-text").classList.remove("blur"); + this.outputTextEl.classList.remove("blur"); } /** @@ -466,7 +591,6 @@ class OutputWaiter { clearTimeout(this.outputLoaderTimeout); const outputLoader = document.getElementById("output-loader"), - outputElement = document.getElementById("output-text"), animation = document.getElementById("output-loader-animation"); if (value) { @@ -483,7 +607,6 @@ class OutputWaiter { // Show the loading screen this.outputLoaderTimeout = setTimeout(function() { - outputElement.disabled = true; outputLoader.style.visibility = "visible"; outputLoader.style.opacity = 1; }, 200); @@ -494,7 +617,6 @@ class OutputWaiter { animation.removeChild(this.bombeEl); } catch (err) {} }.bind(this), 500); - outputElement.disabled = false; outputLoader.style.opacity = 0; outputLoader.style.visibility = "hidden"; } @@ -717,8 +839,7 @@ class OutputWaiter { debounce(this.set, 50, "setOutput", this, [inputNum])(); - document.getElementById("output-html").scroll(0, 0); - document.getElementById("output-text").scroll(0, 0); + this.outputTextEl.scroll(0, 0); // TODO if (changeInput) { this.manager.input.changeTab(inputNum, false); @@ -996,32 +1117,6 @@ class OutputWaiter { } } - /** - * Displays information about the output. - * - * @param {number} length - The length of the current output string - * @param {number} lines - The number of the lines in the current output string - * @param {number} duration - The length of time (ms) it took to generate the output - */ - setOutputInfo(length, lines, duration) { - if (!length) return; - let width = length.toString().length; - width = width < 4 ? 4 : width; - - const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " "); - const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " "); - - let msg = "time: " + timeStr + "
    length: " + lengthStr; - - if (typeof lines === "number") { - const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " "); - msg += "
    lines: " + linesStr; - } - - document.getElementById("output-info").innerHTML = msg; - document.getElementById("output-selection-info").innerHTML = ""; - } - /** * Triggers the BackgroundWorker to attempt Magic on the current output. */ @@ -1111,9 +1206,7 @@ class OutputWaiter { async displayFileSlice() { document.querySelector("#output-loader .loading-msg").textContent = "Loading file slice..."; this.toggleLoader(true); - const outputText = document.getElementById("output-text"), - outputHtml = document.getElementById("output-html"), - outputFile = document.getElementById("output-file"), + const outputFile = document.getElementById("output-file"), outputHighlighter = document.getElementById("output-highlighter"), inputHighlighter = document.getElementById("input-highlighter"), showFileOverlay = document.getElementById("show-file-overlay"), @@ -1130,12 +1223,12 @@ class OutputWaiter { str = Utils.arrayBufferToStr(await this.getDishBuffer(output.dish).slice(sliceFrom, sliceTo)); } - outputText.classList.remove("blur"); + this.outputTextEl.classList.remove("blur"); showFileOverlay.style.display = "block"; - outputText.value = Utils.printable(str, true); + this.clearHTMLOutput(); + this.setOutput(str); - outputText.style.display = "block"; - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; outputHighlighter.display = "block"; inputHighlighter.display = "block"; @@ -1149,9 +1242,7 @@ class OutputWaiter { async showAllFile() { document.querySelector("#output-loader .loading-msg").textContent = "Loading entire file at user instruction. This may cause a crash..."; this.toggleLoader(true); - const outputText = document.getElementById("output-text"), - outputHtml = document.getElementById("output-html"), - outputFile = document.getElementById("output-file"), + const outputFile = document.getElementById("output-file"), outputHighlighter = document.getElementById("output-highlighter"), inputHighlighter = document.getElementById("input-highlighter"), showFileOverlay = document.getElementById("show-file-overlay"), @@ -1164,12 +1255,12 @@ class OutputWaiter { str = Utils.arrayBufferToStr(await this.getDishBuffer(output.dish)); } - outputText.classList.remove("blur"); + this.outputTextEl.classList.remove("blur"); showFileOverlay.style.display = "none"; - outputText.value = Utils.printable(str, true); + this.clearHTMLOutput(); + this.setOutput(str); - outputText.style.display = "block"; - outputHtml.style.display = "none"; + this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; outputHighlighter.display = "block"; inputHighlighter.display = "block"; @@ -1185,7 +1276,7 @@ class OutputWaiter { showFileOverlayClick(e) { const showFileOverlay = e.target; - document.getElementById("output-text").classList.add("blur"); + this.outputTextEl.classList.add("blur"); showFileOverlay.style.display = "none"; this.set(this.manager.tabs.getActiveOutputTab()); } @@ -1212,7 +1303,7 @@ class OutputWaiter { * Handler for copy click events. * Copies the output to the clipboard */ - async copyClick() { + async copyClick() { // TODO - do we need this? const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab()); if (dish === null) { this.app.alert("Could not find data to copy. Has this output been baked yet?", 3000); diff --git a/tests/browser/nightwatch.js b/tests/browser/nightwatch.js index ba6f5204..e63a8036 100644 --- a/tests/browser/nightwatch.js +++ b/tests/browser/nightwatch.js @@ -90,7 +90,7 @@ module.exports = { browser .useCss() .waitForElementNotVisible("#stale-indicator", 1000) - .expect.element("#output-text").to.have.property("value").that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e"); + .expect.element("#output-text").to.have.property("value").that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e"); // TODO // Clear recipe browser @@ -206,7 +206,7 @@ module.exports = { .useCss() .waitForElementVisible(".operation .op-title", 1000) .waitForElementNotVisible("#stale-indicator", 1000) - .expect.element("#output-text").to.have.property("value").which.matches(/[\da-f-]{36}/); + .expect.element("#output-text").to.have.property("value").which.matches(/[\da-f-]{36}/); // TODO browser.click("#clr-recipe"); }, diff --git a/tests/browser/ops.js b/tests/browser/ops.js index d0933bb6..64f8e036 100644 --- a/tests/browser/ops.js +++ b/tests/browser/ops.js @@ -443,9 +443,9 @@ function testOp(browser, opName, input, output, args=[]) { bakeOp(browser, opName, input, args); if (typeof output === "string") { - browser.expect.element("#output-text").to.have.property("value").that.equals(output); + browser.expect.element("#output-text").to.have.property("value").that.equals(output); // TODO } else if (output instanceof RegExp) { - browser.expect.element("#output-text").to.have.property("value").that.matches(output); + browser.expect.element("#output-text").to.have.property("value").that.matches(output); // TODO } } @@ -463,8 +463,8 @@ function testOpHtml(browser, opName, input, cssSelector, output, args=[]) { bakeOp(browser, opName, input, args); if (typeof output === "string") { - browser.expect.element("#output-html " + cssSelector).text.that.equals(output); + browser.expect.element("#output-html " + cssSelector).text.that.equals(output); // TODO } else if (output instanceof RegExp) { - browser.expect.element("#output-html " + cssSelector).text.that.matches(output); + browser.expect.element("#output-html " + cssSelector).text.that.matches(output); // TODO } } From c4414bd910e84c3185af19a234ab90f744dfee94 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 13:53:19 +0100 Subject: [PATCH 165/686] Fixed dropdown toggle height --- src/web/stylesheets/components/_operation.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/stylesheets/components/_operation.css b/src/web/stylesheets/components/_operation.css index 39f53a07..7d45a9e2 100755 --- a/src/web/stylesheets/components/_operation.css +++ b/src/web/stylesheets/components/_operation.css @@ -186,7 +186,7 @@ div.toggle-string { } .ingredients .dropdown-toggle-split { - height: 41px !important; + height: 40px !important; } .boolean-arg { From fc95d82c49794375cb7f533f883576fb42126e8e Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 14:49:40 +0100 Subject: [PATCH 166/686] Tweaked Extract Files minimum size --- src/core/operations/ExtractFiles.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/operations/ExtractFiles.mjs b/src/core/operations/ExtractFiles.mjs index 8c313f59..4c6fd1df 100644 --- a/src/core/operations/ExtractFiles.mjs +++ b/src/core/operations/ExtractFiles.mjs @@ -58,7 +58,7 @@ class ExtractFiles extends Operation { { name: "Minimum File Size", type: "number", - value: 0 + value: 100 } ]); } @@ -86,8 +86,8 @@ class ExtractFiles extends Operation { const errors = []; detectedFiles.forEach(detectedFile => { try { - let file; - if ((file = extractFile(bytes, detectedFile.fileDetails, detectedFile.offset)).size >= minSize) + const file = extractFile(bytes, detectedFile.fileDetails, detectedFile.offset); + if (file.size >= minSize) files.push(file); } catch (err) { if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) { From 50f0f708052ff684e3c1134fdee887e00b5e71b0 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 14:49:50 +0100 Subject: [PATCH 167/686] 9.39.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffde1368..62b4627e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.1", + "version": "9.39.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.1", + "version": "9.39.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index cea3fb19..8f392c43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.1", + "version": "9.39.2", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From a9657ac5c7af0101e011086afae4e2fbc28baa46 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 14:51:08 +0100 Subject: [PATCH 168/686] 9.39.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62b4627e..a56add40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.2", + "version": "9.39.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.2", + "version": "9.39.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 8f392c43..38144a24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.2", + "version": "9.39.3", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 65aeae9c1e9414665dc898fe4036972f10c3376c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 14:53:07 +0100 Subject: [PATCH 169/686] 9.39.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a56add40..db1ff36e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.3", + "version": "9.39.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.3", + "version": "9.39.4", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 38144a24..29d617bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.3", + "version": "9.39.4", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 4b018bf421422600bf114a9053558a4dd13dfb85 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 14:55:32 +0100 Subject: [PATCH 170/686] 9.39.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index db1ff36e..88d50c51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.4", + "version": "9.39.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.4", + "version": "9.39.5", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 29d617bc..2416f720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.4", + "version": "9.39.5", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 2f097e5dfcc03b577478b622c3ae17bffcc64e61 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:15:53 +0100 Subject: [PATCH 171/686] Tidied up Base85 issues --- src/core/operations/FromBase85.mjs | 11 ++++------- tests/lib/TestRegister.mjs | 7 +++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/core/operations/FromBase85.mjs b/src/core/operations/FromBase85.mjs index 01024f1a..f9b37c74 100644 --- a/src/core/operations/FromBase85.mjs +++ b/src/core/operations/FromBase85.mjs @@ -85,6 +85,10 @@ class FromBase85 extends Operation { throw new OperationError("Alphabet must be of length 85"); } + // Remove delimiters if present + const matches = input.match(/^<~(.+?)~>$/); + if (matches !== null) input = matches[1]; + // Remove non-alphabet characters if (removeNonAlphChars) { const re = new RegExp("[^" + alphabet.replace(/[[\]\\\-^$]/g, "\\$&") + "]", "g"); @@ -93,13 +97,6 @@ class FromBase85 extends Operation { if (input.length === 0) return []; - input = input.replace(/\s+/g, ""); - - if (encoding === "Standard") { - const matches = input.match(/<~(.+?)~>/); - if (matches !== null) input = matches[1]; - } - let i = 0; let block, blockBytes; while (i < input.length) { diff --git a/tests/lib/TestRegister.mjs b/tests/lib/TestRegister.mjs index 634e3b62..8b687fcc 100644 --- a/tests/lib/TestRegister.mjs +++ b/tests/lib/TestRegister.mjs @@ -12,6 +12,7 @@ import Chef from "../../src/core/Chef.mjs"; import Utils from "../../src/core/Utils.mjs"; import cliProgress from "cli-progress"; +import log from "loglevel"; /** * Object to store and run the list of tests. @@ -50,6 +51,9 @@ class TestRegister { * Runs all the tests in the register. */ async runTests () { + // Turn off logging to avoid messy errors + log.setLevel("silent", false); + const progBar = new cliProgress.SingleBar({ format: formatter, stopOnComplete: true @@ -128,6 +132,9 @@ class TestRegister { progBar.increment(); } + // Turn logging back on + log.setLevel("info", false); + return testResults; } From 1fb1d9cbb75c49f171112f061a2ab4dc4cd140bd Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:16:00 +0100 Subject: [PATCH 172/686] 9.39.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88d50c51..0efed1ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.5", + "version": "9.39.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.5", + "version": "9.39.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2416f720..d370000d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.5", + "version": "9.39.6", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 7d4e5545715ed6b279e3d6860ae537fd70c986c4 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:26:33 +0100 Subject: [PATCH 173/686] Tweaks to P-List Viewer operation --- src/core/config/Categories.json | 2 +- src/core/operations/PLISTViewer.mjs | 33 +++++++++-------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 240ddbc3..a2c85ea3 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -457,7 +457,7 @@ "Frequency distribution", "Index of Coincidence", "Chi Square", - "PLIST Viewer", + "P-list Viewer", "Disassemble x86", "Pseudo-Random Number Generator", "Generate UUID", diff --git a/src/core/operations/PLISTViewer.mjs b/src/core/operations/PLISTViewer.mjs index b8a90c5b..67a42359 100644 --- a/src/core/operations/PLISTViewer.mjs +++ b/src/core/operations/PLISTViewer.mjs @@ -7,36 +7,23 @@ import Operation from "../Operation.mjs"; /** - * PLIST Viewer operation + * P-list Viewer operation */ -class PLISTViewer extends Operation { +class PlistViewer extends Operation { /** - * PLISTViewer constructor + * PlistViewer constructor */ constructor() { super(); - this.name = "PLIST Viewer"; - this.module = "Other"; - this.description = "Converts PLISTXML file into a human readable format."; - this.infoURL = ""; + this.name = "P-list Viewer"; + this.module = "Default"; + this.description = "In the macOS, iOS, NeXTSTEP, and GNUstep programming frameworks, property list files are files that store serialized objects. Property list files use the filename extension .plist, and thus are often referred to as p-list files.

    This operation displays plist files in a human readable format."; + this.infoURL = "https://wikipedia.org/wiki/Property_list"; this.inputType = "string"; this.outputType = "string"; - this.args = [ - /* Example arguments. See the project wiki for full details. - { - name: "First arg", - type: "string", - value: "Don't Panic" - }, - { - name: "Second arg", - type: "number", - value: 42 - } - */ - ]; + this.args = []; } /** @@ -46,7 +33,7 @@ class PLISTViewer extends Operation { */ run(input, args) { - // Regexes are designed to transform the xml format into a reasonably more readable string format. + // Regexes are designed to transform the xml format into a more readable string format. input = input.slice(input.indexOf("/g, "plist => ") .replace(//g, "{") @@ -143,4 +130,4 @@ class PLISTViewer extends Operation { } } -export default PLISTViewer; +export default PlistViewer; From c9d29c89bb29e379254745af03ff7268797391df Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:27:01 +0100 Subject: [PATCH 174/686] 9.40.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0efed1ea..34863028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.39.6", + "version": "9.40.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.39.6", + "version": "9.40.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index d370000d..375c2c2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.39.6", + "version": "9.40.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 94700dab897c898f55b474be0b1832180ee47dec Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:28:39 +0100 Subject: [PATCH 175/686] Updated CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af8843e5..22506911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.40.0] - 2022-07-08 +- Added 'P-list Viewer' operation [@n1073645] | [#906] + ### [9.39.0] - 2022-06-09 - Added 'ELF Info' operation [@n1073645] | [#1364] @@ -294,6 +297,7 @@ All major and minor version changes will be documented in this file. Details of +[9.40.0]: https://github.com/gchq/CyberChef/releases/tag/v9.40.0 [9.39.0]: https://github.com/gchq/CyberChef/releases/tag/v9.39.0 [9.38.0]: https://github.com/gchq/CyberChef/releases/tag/v9.38.0 [9.37.0]: https://github.com/gchq/CyberChef/releases/tag/v9.37.0 @@ -491,6 +495,7 @@ All major and minor version changes will be documented in this file. Details of [#674]: https://github.com/gchq/CyberChef/pull/674 [#683]: https://github.com/gchq/CyberChef/pull/683 [#865]: https://github.com/gchq/CyberChef/pull/865 +[#906]: https://github.com/gchq/CyberChef/pull/906 [#912]: https://github.com/gchq/CyberChef/pull/912 [#917]: https://github.com/gchq/CyberChef/pull/917 [#934]: https://github.com/gchq/CyberChef/pull/934 From 6cccc2c786ef8dd24547930e3f567157e176b06d Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:36:30 +0100 Subject: [PATCH 176/686] Tidied Caesar Box Cipher --- src/core/operations/CaesarBoxCipher.mjs | 2 +- tests/operations/tests/CaesarBoxCipher.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/CaesarBoxCipher.mjs b/src/core/operations/CaesarBoxCipher.mjs index 9c835b4b..680db900 100644 --- a/src/core/operations/CaesarBoxCipher.mjs +++ b/src/core/operations/CaesarBoxCipher.mjs @@ -19,7 +19,7 @@ class CaesarBoxCipher extends Operation { this.name = "Caesar Box Cipher"; this.module = "Ciphers"; - this.description = "Caesar Box Encryption uses a box, a rectangle (or a square), or at least a size W caracterizing its width."; + this.description = "Caesar Box is a transposition cipher used in the Roman Empire, in which letters of the message are written in rows in a square (or a rectangle) and then, read by column."; this.infoURL = "https://www.dcode.fr/caesar-box-cipher"; this.inputType = "string"; this.outputType = "string"; diff --git a/tests/operations/tests/CaesarBoxCipher.mjs b/tests/operations/tests/CaesarBoxCipher.mjs index 3ccdae66..a7b36ef0 100644 --- a/tests/operations/tests/CaesarBoxCipher.mjs +++ b/tests/operations/tests/CaesarBoxCipher.mjs @@ -1,5 +1,5 @@ /** - * Base58 tests. + * Caesar Box Cipher tests. * * @author n1073645 [n1073645@gmail.com] * From 74bb8d92dc379d256a3974ae2b346c6555c7e94e Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:36:36 +0100 Subject: [PATCH 177/686] 9.41.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34863028..42139c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.40.0", + "version": "9.41.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.40.0", + "version": "9.41.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 375c2c2e..06043fd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.40.0", + "version": "9.41.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 98a95c8bbfc0233835dde1716be0db4b0dec5b23 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:38:12 +0100 Subject: [PATCH 178/686] Updated CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22506911..82730071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.41.0] - 2022-07-08 +- Added 'Caesar Box Cipher' operation [@n1073645] | [#1066] + ### [9.40.0] - 2022-07-08 - Added 'P-list Viewer' operation [@n1073645] | [#906] @@ -297,6 +300,7 @@ All major and minor version changes will be documented in this file. Details of +[9.41.0]: https://github.com/gchq/CyberChef/releases/tag/v9.41.0 [9.40.0]: https://github.com/gchq/CyberChef/releases/tag/v9.40.0 [9.39.0]: https://github.com/gchq/CyberChef/releases/tag/v9.39.0 [9.38.0]: https://github.com/gchq/CyberChef/releases/tag/v9.38.0 @@ -511,6 +515,7 @@ All major and minor version changes will be documented in this file. Details of [#1045]: https://github.com/gchq/CyberChef/pull/1045 [#1049]: https://github.com/gchq/CyberChef/pull/1049 [#1065]: https://github.com/gchq/CyberChef/pull/1065 +[#1066]: https://github.com/gchq/CyberChef/pull/1066 [#1083]: https://github.com/gchq/CyberChef/pull/1083 [#1189]: https://github.com/gchq/CyberChef/pull/1189 [#1242]: https://github.com/gchq/CyberChef/pull/1242 From a6aa40db976e5c9532b62e2845d4e6d3d79cdc3b Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:47:35 +0100 Subject: [PATCH 179/686] Tidied LS47 operations --- src/core/lib/LS47.mjs | 6 +++--- src/core/operations/LS47Decrypt.mjs | 5 ++--- src/core/operations/LS47Encrypt.mjs | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/core/lib/LS47.mjs b/src/core/lib/LS47.mjs index 6696aafc..ac7ca839 100644 --- a/src/core/lib/LS47.mjs +++ b/src/core/lib/LS47.mjs @@ -102,10 +102,10 @@ function checkKey(key) { counts[letters.charAt(i)] = 0; for (const elem of letters) { if (letters.indexOf(elem) === -1) - throw new OperationError("Letter " + elem + " not in LS47!"); + throw new OperationError("Letter " + elem + " not in LS47"); counts[elem]++; if (counts[elem] > 1) - throw new OperationError("Letter duplicated in the key!"); + throw new OperationError("Letter duplicated in the key"); } } @@ -120,7 +120,7 @@ function findPos (key, letter) { const index = key.indexOf(letter); if (index >= 0 && index < 49) return [Math.floor(index/7), index%7]; - throw new OperationError("Letter " + letter + " is not in the key!"); + throw new OperationError("Letter " + letter + " is not in the key"); } /** diff --git a/src/core/operations/LS47Decrypt.mjs b/src/core/operations/LS47Decrypt.mjs index cb92cd27..d5764d7f 100644 --- a/src/core/operations/LS47Decrypt.mjs +++ b/src/core/operations/LS47Decrypt.mjs @@ -20,8 +20,8 @@ class LS47Decrypt extends Operation { this.name = "LS47 Decrypt"; this.module = "Crypto"; - this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; - this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master"; + this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.
    The LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()
    An LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; + this.infoURL = "https://github.com/exaexa/ls47"; this.inputType = "string"; this.outputType = "string"; this.args = [ @@ -44,7 +44,6 @@ class LS47Decrypt extends Operation { * @returns {string} */ run(input, args) { - this.paddingSize = parseInt(args[1], 10); LS47.initTiles(); diff --git a/src/core/operations/LS47Encrypt.mjs b/src/core/operations/LS47Encrypt.mjs index 51283844..02f7d994 100644 --- a/src/core/operations/LS47Encrypt.mjs +++ b/src/core/operations/LS47Encrypt.mjs @@ -20,8 +20,8 @@ class LS47Encrypt extends Operation { this.name = "LS47 Encrypt"; this.module = "Crypto"; - this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.\nThe LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()\nA LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; - this.infoURL = "https://gitea.blesmrt.net/exa/ls47/src/branch/master"; + this.description = "This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple key-expansion algorithm, because remembering passwords is popular. Similar security considerations as with ElsieFour hold.
    The LS47 alphabet consists of following characters: _abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()
    A LS47 key is a permutation of the alphabet that is then represented in a 7x7 grid used for the encryption or decryption."; + this.infoURL = "https://github.com/exaexa/ls47"; this.inputType = "string"; this.outputType = "string"; this.args = [ @@ -49,7 +49,6 @@ class LS47Encrypt extends Operation { * @returns {string} */ run(input, args) { - this.paddingSize = parseInt(args[1], 10); LS47.initTiles(); From b828b50ccc2e0e4096cef212d4932d0ba3c65ec3 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:47:42 +0100 Subject: [PATCH 180/686] 9.42.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42139c37..24730227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.41.0", + "version": "9.42.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.41.0", + "version": "9.42.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 06043fd6..a24c996f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.41.0", + "version": "9.42.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 2ffce23c67bda3a4ccf1f1665234c78b1addfe20 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 15:52:00 +0100 Subject: [PATCH 181/686] Updated CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82730071..0dcf5b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.42.0] - 2022-07-08 +- Added 'LS47 Encrypt' and 'LS47 Decrypt' operations [@n1073645] | [#951] + ### [9.41.0] - 2022-07-08 - Added 'Caesar Box Cipher' operation [@n1073645] | [#1066] @@ -300,6 +303,7 @@ All major and minor version changes will be documented in this file. Details of +[9.42.0]: https://github.com/gchq/CyberChef/releases/tag/v9.42.0 [9.41.0]: https://github.com/gchq/CyberChef/releases/tag/v9.41.0 [9.40.0]: https://github.com/gchq/CyberChef/releases/tag/v9.40.0 [9.39.0]: https://github.com/gchq/CyberChef/releases/tag/v9.39.0 @@ -504,6 +508,7 @@ All major and minor version changes will be documented in this file. Details of [#917]: https://github.com/gchq/CyberChef/pull/917 [#934]: https://github.com/gchq/CyberChef/pull/934 [#948]: https://github.com/gchq/CyberChef/pull/948 +[#951]: https://github.com/gchq/CyberChef/pull/951 [#952]: https://github.com/gchq/CyberChef/pull/952 [#965]: https://github.com/gchq/CyberChef/pull/965 [#966]: https://github.com/gchq/CyberChef/pull/966 From eb5663a1eddd7f9400ced8cddcc40caf200a0eac Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:02:24 +0100 Subject: [PATCH 182/686] Tidied ROT brute forcing ops --- src/core/operations/ROT13BruteForce.mjs | 26 ++++++++++++------------- src/core/operations/ROT47BruteForce.mjs | 26 ++++++++++++------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/core/operations/ROT13BruteForce.mjs b/src/core/operations/ROT13BruteForce.mjs index bdf9d40a..aefe2ab7 100644 --- a/src/core/operations/ROT13BruteForce.mjs +++ b/src/core/operations/ROT13BruteForce.mjs @@ -40,28 +40,28 @@ class ROT13BruteForce extends Operation { value: false }, { - "name": "Sample length", - "type": "number", - "value": 100 + name: "Sample length", + type: "number", + value: 100 }, { - "name": "Sample offset", - "type": "number", - "value": 0 + name: "Sample offset", + type: "number", + value: 0 }, { - "name": "Print amount", - "type": "boolean", - "value": true + name: "Print amount", + type: "boolean", + value: true }, { - "name": "Crib (known plaintext string)", - "type": "string", - "value": "" + name: "Crib (known plaintext string)", + type: "string", + value: "" } ]; } - + /** * @param {byteArray} input * @param {Object[]} args diff --git a/src/core/operations/ROT47BruteForce.mjs b/src/core/operations/ROT47BruteForce.mjs index 5fce5259..5f346e00 100644 --- a/src/core/operations/ROT47BruteForce.mjs +++ b/src/core/operations/ROT47BruteForce.mjs @@ -25,28 +25,28 @@ class ROT47BruteForce extends Operation { this.outputType = "string"; this.args = [ { - "name": "Sample length", - "type": "number", - "value": 100 + name: "Sample length", + type: "number", + value: 100 }, { - "name": "Sample offset", - "type": "number", - "value": 0 + name: "Sample offset", + type: "number", + value: 0 }, { - "name": "Print amount", - "type": "boolean", - "value": true + name: "Print amount", + type: "boolean", + value: true }, { - "name": "Crib (known plaintext string)", - "type": "string", - "value": "" + name: "Crib (known plaintext string)", + type: "string", + value: "" } ]; } - + /** * @param {byteArray} input * @param {Object[]} args From dfd9afc2c42712bfc231b27ee57fadaf39006ea4 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:02:35 +0100 Subject: [PATCH 183/686] 9.43.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24730227..489598a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.42.0", + "version": "9.43.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.42.0", + "version": "9.43.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index a24c996f..5b816668 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.42.0", + "version": "9.43.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From f97ce18ff97648e4a20be36b48b6ad462ca07290 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:03:42 +0100 Subject: [PATCH 184/686] Updated CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcf5b17..9cbe89ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.43.0] - 2022-07-08 +- Added 'ROT13 Brute Force' and 'ROT47 Brute Force' operations [@mikecat] | [#1264] + ### [9.42.0] - 2022-07-08 - Added 'LS47 Encrypt' and 'LS47 Decrypt' operations [@n1073645] | [#951] @@ -303,6 +306,7 @@ All major and minor version changes will be documented in this file. Details of +[9.43.0]: https://github.com/gchq/CyberChef/releases/tag/v9.43.0 [9.42.0]: https://github.com/gchq/CyberChef/releases/tag/v9.42.0 [9.41.0]: https://github.com/gchq/CyberChef/releases/tag/v9.41.0 [9.40.0]: https://github.com/gchq/CyberChef/releases/tag/v9.40.0 @@ -430,6 +434,7 @@ All major and minor version changes will be documented in this file. Details of [@t-8ch]: https://github.com/t-8ch [@hettysymes]: https://github.com/hettysymes [@swesven]: https://github.com/swesven +[@mikecat]: https://github.com/mikecat [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513 @@ -528,3 +533,5 @@ All major and minor version changes will be documented in this file. Details of [#1313]: https://github.com/gchq/CyberChef/pull/1313 [#1326]: https://github.com/gchq/CyberChef/pull/1326 [#1364]: https://github.com/gchq/CyberChef/pull/1364 +[#1264]: https://github.com/gchq/CyberChef/pull/1264 + From a7fc455e05cb461d574d2647a262bb4db39f863c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:24:47 +0100 Subject: [PATCH 185/686] 9.44.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fcbc00c..80c49ca8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.43.0", + "version": "9.44.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.43.0", + "version": "9.44.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index dd37cb00..05a8d6a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.43.0", + "version": "9.44.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From f1d318f2295b19be107dfe09c656b3fadc96c445 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:25:59 +0100 Subject: [PATCH 186/686] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cbe89ed..9f0e9159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.44.0] - 2022-07-08 +- Added 'LZString Compress' and 'LZString Decompress' operations [@crespyl] | [#1266] + ### [9.43.0] - 2022-07-08 - Added 'ROT13 Brute Force' and 'ROT47 Brute Force' operations [@mikecat] | [#1264] @@ -306,6 +309,7 @@ All major and minor version changes will be documented in this file. Details of +[9.44.0]: https://github.com/gchq/CyberChef/releases/tag/v9.44.0 [9.43.0]: https://github.com/gchq/CyberChef/releases/tag/v9.43.0 [9.42.0]: https://github.com/gchq/CyberChef/releases/tag/v9.42.0 [9.41.0]: https://github.com/gchq/CyberChef/releases/tag/v9.41.0 @@ -435,6 +439,7 @@ All major and minor version changes will be documented in this file. Details of [@hettysymes]: https://github.com/hettysymes [@swesven]: https://github.com/swesven [@mikecat]: https://github.com/mikecat +[@crespyl]: https://github.com/crespyl [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513 @@ -534,4 +539,5 @@ All major and minor version changes will be documented in this file. Details of [#1326]: https://github.com/gchq/CyberChef/pull/1326 [#1364]: https://github.com/gchq/CyberChef/pull/1364 [#1264]: https://github.com/gchq/CyberChef/pull/1264 +[#1266]: https://github.com/gchq/CyberChef/pull/1266 From 25086386c64cd8c880034653c331b9b8c280e47b Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:33:16 +0100 Subject: [PATCH 187/686] Tidied ROT8000 --- src/core/operations/ROT8000.mjs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/core/operations/ROT8000.mjs b/src/core/operations/ROT8000.mjs index 1f039de0..322ceaa3 100644 --- a/src/core/operations/ROT8000.mjs +++ b/src/core/operations/ROT8000.mjs @@ -20,7 +20,7 @@ class ROT8000 extends Operation { this.name = "ROT8000"; this.module = "Default"; this.description = "The simple Caesar-cypher encryption that replaces each Unicode character with the one 0x8000 places forward or back along the alphabet."; - this.infoURL = "https://github.com/rottytooth/rot8000"; + this.infoURL = "https://rot8000.com/info"; this.inputType = "string"; this.outputType = "string"; this.args = []; @@ -35,7 +35,25 @@ class ROT8000 extends Operation { // Inspired from https://github.com/rottytooth/rot8000/blob/main/rot8000.js // these come from the valid-code-point-transitions.json file generated from the c# proj // this is done bc: 1) don't trust JS's understanging of surrogate pairs and 2) consistency with original rot8000 - const validCodePoints = JSON.parse('{"33":true,"127":false,"161":true,"5760":false,"5761":true,"8192":false,"8203":true,"8232":false,"8234":true,"8239":false,"8240":true,"8287":false,"8288":true,"12288":false,"12289":true,"55296":false,"57344":true}'); + const validCodePoints = { + "33": true, + "127": false, + "161": true, + "5760": false, + "5761": true, + "8192": false, + "8203": true, + "8232": false, + "8234": true, + "8239": false, + "8240": true, + "8287": false, + "8288": true, + "12288": false, + "12289": true, + "55296": false, + "57344": true + }; const bmpSize = 0x10000; const rotList = {}; // the mapping of char to rotated char const hiddenBlocks = []; From 6a10e94bfd902b52e3af04e79d47524b7ddf29e1 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:33:33 +0100 Subject: [PATCH 188/686] 9.45.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80c49ca8..6b3a5d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.44.0", + "version": "9.45.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.44.0", + "version": "9.45.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 05a8d6a1..2bd3ca72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.44.0", + "version": "9.45.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 683bd3e5db089a83794e9884c0c3d89a309acbef Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 16:34:21 +0100 Subject: [PATCH 189/686] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0e9159..28a07ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.45.0] - 2022-07-08 +- Added 'ROT8000' operation [@thomasleplus] | [#1250] + ### [9.44.0] - 2022-07-08 - Added 'LZString Compress' and 'LZString Decompress' operations [@crespyl] | [#1266] @@ -309,6 +312,7 @@ All major and minor version changes will be documented in this file. Details of +[9.45.0]: https://github.com/gchq/CyberChef/releases/tag/v9.45.0 [9.44.0]: https://github.com/gchq/CyberChef/releases/tag/v9.44.0 [9.43.0]: https://github.com/gchq/CyberChef/releases/tag/v9.43.0 [9.42.0]: https://github.com/gchq/CyberChef/releases/tag/v9.42.0 @@ -440,6 +444,7 @@ All major and minor version changes will be documented in this file. Details of [@swesven]: https://github.com/swesven [@mikecat]: https://github.com/mikecat [@crespyl]: https://github.com/crespyl +[@thomasleplus]: https://github.com/thomasleplus [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513 @@ -540,4 +545,5 @@ All major and minor version changes will be documented in this file. Details of [#1364]: https://github.com/gchq/CyberChef/pull/1364 [#1264]: https://github.com/gchq/CyberChef/pull/1264 [#1266]: https://github.com/gchq/CyberChef/pull/1266 +[#1250]: https://github.com/gchq/CyberChef/pull/1250 From 4200ed4eb9881a4065a9cae0765cfbf56b365f61 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 17:16:35 +0100 Subject: [PATCH 190/686] Tidied Cetacean ciphers --- package-lock.json | 11 ++++++++- src/core/config/Categories.json | 4 ++-- src/core/operations/CetaceanCipherDecode.mjs | 23 +++++++++--------- src/core/operations/CetaceanCipherEncode.mjs | 25 +++++++++----------- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b3a5d60..cb17b30e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "lodash": "^4.17.21", "loglevel": "^1.8.0", "loglevel-message-prefix": "^3.0.0", + "lz-string": "^1.4.4", "markdown-it": "^13.0.1", "moment": "^2.29.3", "moment-timezone": "^0.5.34", @@ -10120,6 +10121,14 @@ "node": ">=10" } }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -23564,7 +23573,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==" }, "make-dir": { "version": "3.1.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 19ab89d3..8ac60048 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -77,8 +77,6 @@ "Blowfish Decrypt", "DES Encrypt", "DES Decrypt", - "Cetacean Cipher Encode", - "Cetacean Cipher Decode", "Triple DES Encrypt", "Triple DES Decrypt", "LS47 Encrypt", @@ -114,6 +112,8 @@ "Atbash Cipher", "CipherSaber2 Encrypt", "CipherSaber2 Decrypt", + "Cetacean Cipher Encode", + "Cetacean Cipher Decode", "Substitute", "Derive PBKDF2 key", "Derive EVP key", diff --git a/src/core/operations/CetaceanCipherDecode.mjs b/src/core/operations/CetaceanCipherDecode.mjs index a79b98c5..a50fe6b7 100644 --- a/src/core/operations/CetaceanCipherDecode.mjs +++ b/src/core/operations/CetaceanCipherDecode.mjs @@ -20,7 +20,7 @@ class CetaceanCipherDecode extends Operation { this.name = "Cetacean Cipher Decode"; this.module = "Ciphers"; this.description = "Decode Cetacean Cipher input.

    e.g. EEEEEEEEEeeEeEEEEEEEEEEEEeeEeEEe becomes hi"; - this.infoURL = ""; + this.infoURL = "https://hitchhikers.fandom.com/wiki/Dolphins"; this.inputType = "string"; this.outputType = "string"; @@ -30,7 +30,7 @@ class CetaceanCipherDecode extends Operation { flags: "", args: [] } - ] + ]; } /** @@ -40,24 +40,23 @@ class CetaceanCipherDecode extends Operation { */ run(input, args) { const binaryArray = []; - for ( const char of input ) { - if ( char === ' ' ) { - binaryArray.push(...[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 ]); + for (const char of input) { + if (char === " ") { + binaryArray.push(...[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]); } else { - binaryArray.push( char === 'e' ? 1 : 0 ); + binaryArray.push(char === "e" ? 1 : 0); } } const byteArray = []; - for ( let i = 0; i < binaryArray.length; i += 16 ) { - byteArray.push(binaryArray.slice(i, i + 16).join('')) + for (let i = 0; i < binaryArray.length; i += 16) { + byteArray.push(binaryArray.slice(i, i + 16).join("")); } - return byteArray.map( byte => - String.fromCharCode(parseInt( byte , 2 ) - ) - ).join(''); + return byteArray.map(byte => + String.fromCharCode(parseInt(byte, 2)) + ).join(""); } } diff --git a/src/core/operations/CetaceanCipherEncode.mjs b/src/core/operations/CetaceanCipherEncode.mjs index e32e4f81..ec5f76d6 100644 --- a/src/core/operations/CetaceanCipherEncode.mjs +++ b/src/core/operations/CetaceanCipherEncode.mjs @@ -5,6 +5,7 @@ */ import Operation from "../Operation.mjs"; +import {toBinary} from "../lib/Binary.mjs"; /** * Cetacean Cipher Encode operation @@ -19,8 +20,8 @@ class CetaceanCipherEncode extends Operation { this.name = "Cetacean Cipher Encode"; this.module = "Ciphers"; - this.description = "Converts any input into Cetacean Cipher.

    e.g. hi becomes EEEEEEEEEeeEeEEEEEEEEEEEEeeEeEEe\""; - this.infoURL = ""; + this.description = "Converts any input into Cetacean Cipher.

    e.g. hi becomes EEEEEEEEEeeEeEEEEEEEEEEEEeeEeEEe"; + this.infoURL = "https://hitchhikers.fandom.com/wiki/Dolphins"; this.inputType = "string"; this.outputType = "string"; } @@ -31,23 +32,19 @@ class CetaceanCipherEncode extends Operation { * @returns {string} */ run(input, args) { - let result = []; - let charArray = input.split(''); + const result = []; + const charArray = input.split(""); - charArray.map( ( character ) => { - if ( character === ' ' ) { - result.push( character ); + charArray.map(character => { + if (character === " ") { + result.push(character); } else { - const binaryArray = this.encodeToBinary( character ).split(''); - result.push( binaryArray.map(( str ) => str === '1' ? 'e' : 'E' ).join('')); + const binaryArray = toBinary(character.charCodeAt(0), "None", 16).split(""); + result.push(binaryArray.map(str => str === "1" ? "e" : "E").join("")); } }); - return result.join(''); - } - - encodeToBinary( char, padding = 16 ) { - return char.charCodeAt(0).toString(2).padStart( padding, '0'); + return result.join(""); } } From 85496684d8f184e134a9a38f7c6fbf235ba611fa Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 17:17:23 +0100 Subject: [PATCH 191/686] 9.46.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb17b30e..e1712692 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.45.0", + "version": "9.46.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.45.0", + "version": "9.46.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2bd3ca72..48d6f693 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.45.0", + "version": "9.46.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 037590f83128fb856f2e590f133201a39d2c7d2a Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 8 Jul 2022 17:18:20 +0100 Subject: [PATCH 192/686] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a07ec7..f5d0712d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [9.46.0] - 2022-07-08 +- Added 'Cetacean Cipher Encode' and 'Cetacean Cipher Decode' operations [@valdelaseras] | [#1308] + ### [9.45.0] - 2022-07-08 - Added 'ROT8000' operation [@thomasleplus] | [#1250] @@ -312,6 +315,7 @@ All major and minor version changes will be documented in this file. Details of +[9.46.0]: https://github.com/gchq/CyberChef/releases/tag/v9.46.0 [9.45.0]: https://github.com/gchq/CyberChef/releases/tag/v9.45.0 [9.44.0]: https://github.com/gchq/CyberChef/releases/tag/v9.44.0 [9.43.0]: https://github.com/gchq/CyberChef/releases/tag/v9.43.0 @@ -445,6 +449,7 @@ All major and minor version changes will be documented in this file. Details of [@mikecat]: https://github.com/mikecat [@crespyl]: https://github.com/crespyl [@thomasleplus]: https://github.com/thomasleplus +[@valdelaseras]: https://github.com/valdelaseras [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513 @@ -546,4 +551,5 @@ All major and minor version changes will be documented in this file. Details of [#1264]: https://github.com/gchq/CyberChef/pull/1264 [#1266]: https://github.com/gchq/CyberChef/pull/1266 [#1250]: https://github.com/gchq/CyberChef/pull/1250 +[#1308]: https://github.com/gchq/CyberChef/pull/1308 From 890f645eebd6665f9fffbebcfb200a518190f008 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Sun, 10 Jul 2022 22:01:22 +0100 Subject: [PATCH 193/686] Overhauled Highlighting to work with new editor and support multiple selections --- src/core/ChefWorker.js | 2 +- src/core/operations/ToHex.mjs | 6 +- src/web/Manager.mjs | 8 - src/web/waiters/HighlighterWaiter.mjs | 419 +++++--------------------- src/web/waiters/InputWaiter.mjs | 24 +- src/web/waiters/OutputWaiter.mjs | 33 +- src/web/waiters/TabWaiter.mjs | 3 - src/web/waiters/WorkerWaiter.mjs | 2 +- 8 files changed, 104 insertions(+), 393 deletions(-) diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index f4a17f63..d46a705d 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -186,7 +186,7 @@ async function getDishTitle(data) { * * @param {Object[]} recipeConfig * @param {string} direction - * @param {Object} pos - The position object for the highlight. + * @param {Object[]} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ diff --git a/src/core/operations/ToHex.mjs b/src/core/operations/ToHex.mjs index 71893105..092155a9 100644 --- a/src/core/operations/ToHex.mjs +++ b/src/core/operations/ToHex.mjs @@ -76,7 +76,7 @@ class ToHex extends Operation { } const lineSize = args[1], - len = (delim === "\r\n" ? 1 : delim.length) + commaLen; + len = delim.length + commaLen; const countLF = function(p) { // Count the number of LFs from 0 upto p @@ -105,7 +105,7 @@ class ToHex extends Operation { * @returns {Object[]} pos */ highlightReverse(pos, args) { - let delim, commaLen; + let delim, commaLen = 0; if (args[0] === "0x with comma") { delim = "0x"; commaLen = 1; @@ -114,7 +114,7 @@ class ToHex extends Operation { } const lineSize = args[1], - len = (delim === "\r\n" ? 1 : delim.length) + commaLen, + len = delim.length + commaLen, width = len + 2; const countLF = function(p) { diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 2477bb60..a46379e9 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -153,10 +153,6 @@ class Manager { this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input); this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input); this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input); - document.getElementById("input-text").addEventListener("scroll", this.highlighter.inputScroll.bind(this.highlighter)); - document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter)); - document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter)); - this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter); document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input)); document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input)); document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input)); @@ -188,10 +184,6 @@ class Manager { document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output)); document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output)); document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output)); - document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter)); - document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter)); - document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter)); - this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter); this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); this.addDynamicListener("#output-file-show-all", "click", this.output.showAllFile, this.output); this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output); diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs index d1340165..8b4375fe 100755 --- a/src/web/waiters/HighlighterWaiter.mjs +++ b/src/web/waiters/HighlighterWaiter.mjs @@ -4,17 +4,7 @@ * @license Apache-2.0 */ -/** - * HighlighterWaiter data type enum for the input. - * @enum - */ -const INPUT = 0; - -/** - * HighlighterWaiter data type enum for the output. - * @enum - */ -const OUTPUT = 1; +import {EditorSelection} from "@codemirror/state"; /** @@ -32,309 +22,81 @@ class HighlighterWaiter { this.app = app; this.manager = manager; - this.mouseButtonDown = false; - this.mouseTarget = null; + this.currentSelectionRanges = []; } - /** - * Determines if the current text selection is running backwards or forwards. - * StackOverflow answer id: 12652116 + * Handler for selection change events in the input and output * - * @private - * @returns {boolean} - */ - _isSelectionBackwards() { - let backwards = false; - const sel = window.getSelection(); - - if (!sel.isCollapsed) { - const range = document.createRange(); - range.setStart(sel.anchorNode, sel.anchorOffset); - range.setEnd(sel.focusNode, sel.focusOffset); - backwards = range.collapsed; - range.detach(); - } - return backwards; - } - - - /** - * Calculates the text offset of a position in an HTML element, ignoring HTML tags. - * - * @private - * @param {element} node - The parent HTML node. - * @param {number} offset - The offset since the last HTML element. - * @returns {number} - */ - _getOutputHtmlOffset(node, offset) { - const sel = window.getSelection(); - const range = document.createRange(); - - range.selectNodeContents(document.getElementById("output-html")); - range.setEnd(node, offset); - sel.removeAllRanges(); - sel.addRange(range); - - return sel.toString().length; - } - - - /** - * Gets the current selection offsets in the output HTML, ignoring HTML tags. - * - * @private - * @returns {Object} pos - * @returns {number} pos.start - * @returns {number} pos.end - */ - _getOutputHtmlSelectionOffsets() { - const sel = window.getSelection(); - let range, - start = 0, - end = 0, - backwards = false; - - if (sel.rangeCount) { - range = sel.getRangeAt(sel.rangeCount - 1); - backwards = this._isSelectionBackwards(); - start = this._getOutputHtmlOffset(range.startContainer, range.startOffset); - end = this._getOutputHtmlOffset(range.endContainer, range.endOffset); - sel.removeAllRanges(); - sel.addRange(range); - - if (backwards) { - // If selecting backwards, reverse the start and end offsets for the selection to - // prevent deselecting as the drag continues. - sel.collapseToEnd(); - sel.extend(sel.anchorNode, range.startOffset); - } - } - - return { - start: start, - end: end - }; - } - - - /** - * Handler for input scroll events. - * Scrolls the highlighter pane to match the input textarea position. - * - * @param {event} e - */ - inputScroll(e) { - const el = e.target; - document.getElementById("input-highlighter").scrollTop = el.scrollTop; - document.getElementById("input-highlighter").scrollLeft = el.scrollLeft; - } - - - /** - * Handler for output scroll events. - * Scrolls the highlighter pane to match the output textarea position. - * - * @param {event} e - */ - outputScroll(e) { - const el = e.target; - document.getElementById("output-highlighter").scrollTop = el.scrollTop; - document.getElementById("output-highlighter").scrollLeft = el.scrollLeft; - } - - - /** - * Handler for input mousedown events. - * Calculates the current selection info, and highlights the corresponding data in the output. - * - * @param {event} e - */ - inputMousedown(e) { - this.mouseButtonDown = true; - this.mouseTarget = INPUT; - this.removeHighlights(); - - const sel = document.getSelection(); - const start = sel.baseOffset; - const end = sel.extentOffset; - - if (start !== 0 || end !== 0) { - this.highlightOutput([{start: start, end: end}]); - } - } - - - /** - * Handler for output mousedown events. - * Calculates the current selection info, and highlights the corresponding data in the input. - * - * @param {event} e - */ - outputMousedown(e) { - this.mouseButtonDown = true; - this.mouseTarget = OUTPUT; - this.removeHighlights(); - - const sel = document.getSelection(); - const start = sel.baseOffset; - const end = sel.extentOffset; - - if (start !== 0 || end !== 0) { - this.highlightInput([{start: start, end: end}]); - } - } - - - /** - * Handler for input mouseup events. - * - * @param {event} e - */ - inputMouseup(e) { - this.mouseButtonDown = false; - } - - - /** - * Handler for output mouseup events. - * - * @param {event} e - */ - outputMouseup(e) { - this.mouseButtonDown = false; - } - - - /** - * Handler for input mousemove events. - * Calculates the current selection info, and highlights the corresponding data in the output. - * - * @param {event} e - */ - inputMousemove(e) { - // Check that the left mouse button is pressed - if (!this.mouseButtonDown || - e.which !== 1 || - this.mouseTarget !== INPUT) - return; - - const sel = document.getSelection(); - const start = sel.baseOffset; - const end = sel.extentOffset; - - if (start !== 0 || end !== 0) { - this.highlightOutput([{start: start, end: end}]); - } - } - - - /** - * Handler for output mousemove events. - * Calculates the current selection info, and highlights the corresponding data in the input. - * - * @param {event} e - */ - outputMousemove(e) { - // Check that the left mouse button is pressed - if (!this.mouseButtonDown || - e.which !== 1 || - this.mouseTarget !== OUTPUT) - return; - - const sel = document.getSelection(); - const start = sel.baseOffset; - const end = sel.extentOffset; - - if (start !== 0 || end !== 0) { - this.highlightInput([{start: start, end: end}]); - } - } - - - /** - * Given start and end offsets, writes the HTML for the selection info element with the correct - * padding. - * - * @param {number} start - The start offset. - * @param {number} end - The end offset. - * @returns {string} - */ - selectionInfo(start, end) { - const len = end.toString().length; - const width = len < 2 ? 2 : len; - const startStr = start.toString().padStart(width, " ").replace(/ /g, " "); - const endStr = end.toString().padStart(width, " ").replace(/ /g, " "); - const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, " "); - - return "start: " + startStr + "
    end: " + endStr + "
    length: " + lenStr; - } - - - /** - * Removes highlighting and selection information. - */ - removeHighlights() { - document.getElementById("input-highlighter").innerHTML = ""; - document.getElementById("output-highlighter").innerHTML = ""; - } - - - /** - * Highlights the given offsets in the output. + * Highlights the given offsets in the input or output. * We will only highlight if: * - input hasn't changed since last bake * - last bake was a full bake * - all operations in the recipe support highlighting * - * @param {Object} pos - The position object for the highlight. - * @param {number} pos.start - The start offset. - * @param {number} pos.end - The end offset. + * @param {string} io + * @param {ViewUpdate} e */ - highlightOutput(pos) { + selectionChange(io, e) { + // Confirm we are not currently baking if (!this.app.autoBake_ || this.app.baking) return false; - this.manager.worker.highlight(this.app.getRecipeConfig(), "forward", pos); + + // Confirm this was a user-generated event to prevent looping + // from setting the selection in this class + if (!e.transactions[0].isUserEvent("select")) return false; + + const view = io === "input" ? + this.manager.output.outputEditorView : + this.manager.input.inputEditorView; + + this.currentSelectionRanges = []; + + // Confirm some non-empty ranges are set + const selectionRanges = e.state.selection.ranges.filter(r => !r.empty); + if (!selectionRanges.length) { + this.resetSelections(view); + return; + } + + // Loop through ranges and send request for output offsets for each one + const direction = io === "input" ? "forward" : "reverse"; + for (const range of selectionRanges) { + const pos = [{ + start: range.from, + end: range.to + }]; + this.manager.worker.highlight(this.app.getRecipeConfig(), direction, pos); + } } - /** - * Highlights the given offsets in the input. - * We will only highlight if: - * - input hasn't changed since last bake - * - last bake was a full bake - * - all operations in the recipe support highlighting - * - * @param {Object} pos - The position object for the highlight. - * @param {number} pos.start - The start offset. - * @param {number} pos.end - The end offset. + * Resets the current set of selections in the given view + * @param {EditorView} view */ - highlightInput(pos) { - if (!this.app.autoBake_ || this.app.baking) return false; - this.manager.worker.highlight(this.app.getRecipeConfig(), "reverse", pos); + resetSelections(view) { + this.currentSelectionRanges = []; + + // Clear current selection in output or input + view.dispatch({ + selection: EditorSelection.create([EditorSelection.range(0, 0)]) + }); } /** * Displays highlight offsets sent back from the Chef. * - * @param {Object} pos - The position object for the highlight. + * @param {Object[]} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. * @param {string} direction */ displayHighlights(pos, direction) { if (!pos) return; - if (this.manager.tabs.getActiveInputTab() !== this.manager.tabs.getActiveOutputTab()) return; const io = direction === "forward" ? "output" : "input"; - - // TODO - // document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end); - this.highlight( - document.getElementById(io + "-text"), - document.getElementById(io + "-highlighter"), - pos); + this.highlight(io, pos); } @@ -342,74 +104,35 @@ class HighlighterWaiter { * Adds the relevant HTML to the specified highlight element such that highlighting appears * underneath the correct offset. * - * @param {element} textarea - The input or output textarea. - * @param {element} highlighter - The input or output highlighter element. - * @param {Object} pos - The position object for the highlight. - * @param {number} pos.start - The start offset. - * @param {number} pos.end - The end offset. + * @param {string} io - The input or output + * @param {Object[]} ranges - An array of position objects to highlight + * @param {number} ranges.start - The start offset + * @param {number} ranges.end - The end offset */ - async highlight(textarea, highlighter, pos) { - // if (!this.app.options.showHighlighter) return false; - // if (!this.app.options.attemptHighlight) return false; + async highlight(io, ranges) { + if (!this.app.options.showHighlighter) return false; + if (!this.app.options.attemptHighlight) return false; + if (!ranges || !ranges.length) return false; - // // Check if there is a carriage return in the output dish as this will not - // // be displayed by the HTML textarea and will mess up highlighting offsets. - // if (await this.manager.output.containsCR()) return false; + const view = io === "input" ? + this.manager.input.inputEditorView : + this.manager.output.outputEditorView; - // const startPlaceholder = "[startHighlight]"; - // const startPlaceholderRegex = /\[startHighlight\]/g; - // const endPlaceholder = "[endHighlight]"; - // const endPlaceholderRegex = /\[endHighlight\]/g; - // // let text = textarea.value; // TODO + // Add new SelectionRanges to existing ones + for (const range of ranges) { + if (!range.start || !range.end) continue; + this.currentSelectionRanges.push( + EditorSelection.range(range.start, range.end) + ); + } - // // Put placeholders in position - // // If there's only one value, select that - // // If there are multiple, ignore the first one and select all others - // if (pos.length === 1) { - // if (pos[0].end < pos[0].start) return; - // text = text.slice(0, pos[0].start) + - // startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder + - // text.slice(pos[0].end, text.length); - // } else { - // // O(n^2) - Can anyone improve this without overwriting placeholders? - // let result = "", - // endPlaced = true; - - // for (let i = 0; i < text.length; i++) { - // for (let j = 1; j < pos.length; j++) { - // if (pos[j].end < pos[j].start) continue; - // if (pos[j].start === i) { - // result += startPlaceholder; - // endPlaced = false; - // } - // if (pos[j].end === i) { - // result += endPlaceholder; - // endPlaced = true; - // } - // } - // result += text[i]; - // } - // if (!endPlaced) result += endPlaceholder; - // text = result; - // } - - // const cssClass = "hl1"; - - // // Remove HTML tags - // text = text - // .replace(/&/g, "&") - // .replace(//g, ">") - // .replace(/\n/g, " ") - // // Convert placeholders to tags - // .replace(startPlaceholderRegex, "") - // .replace(endPlaceholderRegex, "") + " "; - - // // Adjust width to allow for scrollbars - // highlighter.style.width = textarea.clientWidth + "px"; - // highlighter.innerHTML = text; - // highlighter.scrollTop = textarea.scrollTop; - // highlighter.scrollLeft = textarea.scrollLeft; + // Set selection + if (this.currentSelectionRanges.length) { + view.dispatch({ + selection: EditorSelection.create(this.currentSelectionRanges), + scrollIntoView: true + }); + } } } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 0dc44dbe..ff512f69 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -87,6 +87,7 @@ class InputWaiter { const initialState = EditorState.create({ doc: null, extensions: [ + // Editor extensions history(), highlightSpecialChars({render: renderSpecialChar}), drawSelection(), @@ -95,13 +96,19 @@ class InputWaiter { bracketMatching(), highlightSelectionMatches(), search({top: true}), + EditorState.allowMultipleSelections.of(true), + + // Custom extensions statusBar({ label: "Input", eolHandler: this.eolChange.bind(this) }), + + // Mutable state this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping), this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), - EditorState.allowMultipleSelections.of(true), + + // Keymap keymap.of([ // Explicitly insert a tab rather than indenting the line { key: "Tab", run: insertTab }, @@ -112,6 +119,12 @@ class InputWaiter { ...defaultKeymap, ...searchKeymap ]), + + // Event listeners + EditorView.updateListener.of(e => { + if (e.selectionSet) + this.manager.highlighter.selectionChange("input", e); + }) ] }); @@ -771,9 +784,6 @@ class InputWaiter { const fileOverlay = document.getElementById("input-file"); if (fileOverlay.style.display === "block") return; - // Remove highlighting from input and output panes as the offsets might be different now - this.manager.highlighter.removeHighlights(); - const value = this.getInput(); const activeTab = this.manager.tabs.getActiveInputTab(); @@ -1033,9 +1043,6 @@ class InputWaiter { this.manager.output.removeAllOutputs(); this.manager.output.terminateZipWorker(); - this.manager.highlighter.removeHighlights(); - getSelection().removeAllRanges(); - const tabsList = document.getElementById("input-tabs"); const tabsListChildren = tabsList.children; @@ -1073,9 +1080,6 @@ class InputWaiter { const inputNum = this.manager.tabs.getActiveInputTab(); if (inputNum === -1) return; - this.manager.highlighter.removeHighlights(); - getSelection().removeAllRanges(); - this.updateInputValue(inputNum, "", true); this.set({ diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 496b0ac5..d1fd2532 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -67,6 +67,7 @@ class OutputWaiter { const initialState = EditorState.create({ doc: null, extensions: [ + // Editor extensions EditorState.readOnly.of(true), htmlPlugin(this.htmlOutput), highlightSpecialChars({render: renderSpecialChar}), @@ -76,18 +77,30 @@ class OutputWaiter { bracketMatching(), highlightSelectionMatches(), search({top: true}), + EditorState.allowMultipleSelections.of(true), + + // Custom extensiosn statusBar({ label: "Output", bakeStats: this.bakeStats, eolHandler: this.eolChange.bind(this) }), + + // Mutable state this.outputEditorConf.lineWrapping.of(EditorView.lineWrapping), this.outputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), - EditorState.allowMultipleSelections.of(true), + + // Keymap keymap.of([ ...defaultKeymap, ...searchKeymap ]), + + // Event listeners + EditorView.updateListener.of(e => { + if (e.selectionSet) + this.manager.highlighter.selectionChange("output", e); + }) ] }); @@ -817,9 +830,6 @@ class OutputWaiter { this.hideMagicButton(); - this.manager.highlighter.removeHighlights(); - getSelection().removeAllRanges(); - if (!this.manager.tabs.changeOutputTab(inputNum)) { let direction = "right"; if (currentNum > inputNum) { @@ -1343,21 +1353,6 @@ class OutputWaiter { document.body.removeChild(textarea); } - /** - * Returns true if the output contains carriage returns - * - * @returns {boolean} - */ - async containsCR() { - const dish = this.getOutputDish(this.manager.tabs.getActiveOutputTab()); - if (dish === null) return; - - if (dish.type === Dish.STRING) { - const data = await dish.get(Dish.STRING); - return data.indexOf("\r") >= 0; - } - } - /** * Handler for switch click events. * Moves the current output into the input textarea. diff --git a/src/web/waiters/TabWaiter.mjs b/src/web/waiters/TabWaiter.mjs index 384b1ab7..f5b0efd4 100644 --- a/src/web/waiters/TabWaiter.mjs +++ b/src/web/waiters/TabWaiter.mjs @@ -305,9 +305,6 @@ class TabWaiter { changeTab(inputNum, io) { const tabsList = document.getElementById(`${io}-tabs`); - this.manager.highlighter.removeHighlights(); - getSelection().removeAllRanges(); - let found = false; for (let i = 0; i < tabsList.children.length; i++) { const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10); diff --git a/src/web/waiters/WorkerWaiter.mjs b/src/web/waiters/WorkerWaiter.mjs index 7fcaa509..a63bfc1f 100644 --- a/src/web/waiters/WorkerWaiter.mjs +++ b/src/web/waiters/WorkerWaiter.mjs @@ -794,7 +794,7 @@ class WorkerWaiter { * * @param {Object[]} recipeConfig * @param {string} direction - * @param {Object} pos - The position object for the highlight. + * @param {Object[]} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ From 157dacb3a52fd082ffd203d5a88e328217260eb2 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Mon, 11 Jul 2022 11:43:48 +0100 Subject: [PATCH 194/686] Improved highlighting colours and selection ranges --- src/web/stylesheets/layout/_io.css | 22 +++++++++++ src/web/stylesheets/themes/_classic.css | 10 ++--- src/web/waiters/HighlighterWaiter.mjs | 51 ++++++++++--------------- src/web/waiters/InputWaiter.mjs | 3 +- 4 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index ba670f3d..cb196709 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -440,6 +440,28 @@ filter: brightness(98%); } +/* Highlighting */ +.ͼ2.cm-focused .cm-selectionBackground { + background-color: var(--hl5); +} + +.ͼ2 .cm-selectionBackground { + background-color: var(--hl1); +} + +.ͼ1 .cm-selectionMatch { + background-color: var(--hl2); +} + +.ͼ1.cm-focused .cm-cursor.cm-cursor-primary { + border-color: var(--primary-font-colour); +} + +.ͼ1 .cm-cursor.cm-cursor-primary { + display: block; + border-color: var(--subtext-font-colour); +} + /* Status bar */ diff --git a/src/web/stylesheets/themes/_classic.css b/src/web/stylesheets/themes/_classic.css index 3b3bd555..971c1c57 100755 --- a/src/web/stylesheets/themes/_classic.css +++ b/src/web/stylesheets/themes/_classic.css @@ -110,11 +110,11 @@ /* Highlighter colours */ - --hl1: #fff000; - --hl2: #95dfff; - --hl3: #ffb6b6; - --hl4: #fcf8e3; - --hl5: #8de768; + --hl1: #ffee00aa; + --hl2: #95dfffaa; + --hl3: #ffb6b6aa; + --hl4: #fcf8e3aa; + --hl5: #8de768aa; /* Scrollbar */ diff --git a/src/web/waiters/HighlighterWaiter.mjs b/src/web/waiters/HighlighterWaiter.mjs index 8b4375fe..189d3777 100755 --- a/src/web/waiters/HighlighterWaiter.mjs +++ b/src/web/waiters/HighlighterWaiter.mjs @@ -45,18 +45,10 @@ class HighlighterWaiter { // from setting the selection in this class if (!e.transactions[0].isUserEvent("select")) return false; - const view = io === "input" ? - this.manager.output.outputEditorView : - this.manager.input.inputEditorView; - this.currentSelectionRanges = []; // Confirm some non-empty ranges are set - const selectionRanges = e.state.selection.ranges.filter(r => !r.empty); - if (!selectionRanges.length) { - this.resetSelections(view); - return; - } + const selectionRanges = e.state.selection.ranges; // Loop through ranges and send request for output offsets for each one const direction = io === "input" ? "forward" : "reverse"; @@ -69,19 +61,6 @@ class HighlighterWaiter { } } - /** - * Resets the current set of selections in the given view - * @param {EditorView} view - */ - resetSelections(view) { - this.currentSelectionRanges = []; - - // Clear current selection in output or input - view.dispatch({ - selection: EditorSelection.create([EditorSelection.range(0, 0)]) - }); - } - /** * Displays highlight offsets sent back from the Chef. @@ -120,18 +99,30 @@ class HighlighterWaiter { // Add new SelectionRanges to existing ones for (const range of ranges) { - if (!range.start || !range.end) continue; - this.currentSelectionRanges.push( - EditorSelection.range(range.start, range.end) - ); + if (typeof range.start !== "number" || + typeof range.end !== "number") + continue; + const selection = range.end <= range.start ? + EditorSelection.cursor(range.start) : + EditorSelection.range(range.start, range.end); + + this.currentSelectionRanges.push(selection); } // Set selection if (this.currentSelectionRanges.length) { - view.dispatch({ - selection: EditorSelection.create(this.currentSelectionRanges), - scrollIntoView: true - }); + try { + view.dispatch({ + selection: EditorSelection.create(this.currentSelectionRanges), + scrollIntoView: true + }); + } catch (err) { + // Ignore Range Errors + if (!err.toString().startsWith("RangeError")) { + console.error(err); + } + + } } } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index ff512f69..69417b92 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -12,7 +12,7 @@ import {toBase64} from "../../core/lib/Base64.mjs"; import {isImage} from "../../core/lib/FileType.mjs"; import { - EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor + EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor, dropCursor } from "@codemirror/view"; import {EditorState, Compartment} from "@codemirror/state"; import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@codemirror/commands"; @@ -93,6 +93,7 @@ class InputWaiter { drawSelection(), rectangularSelection(), crosshairCursor(), + dropCursor(), bracketMatching(), highlightSelectionMatches(), search({top: true}), From 5c8aac5572b687186d390d07c7206e068df25a19 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Mon, 11 Jul 2022 13:43:19 +0100 Subject: [PATCH 195/686] Improved input change update responsiveness --- src/core/operations/ParseColourCode.mjs | 2 +- src/web/App.mjs | 9 +++-- src/web/Manager.mjs | 1 - src/web/waiters/InputWaiter.mjs | 53 ++++++------------------- src/web/waiters/OutputWaiter.mjs | 4 +- 5 files changed, 20 insertions(+), 49 deletions(-) diff --git a/src/core/operations/ParseColourCode.mjs b/src/core/operations/ParseColourCode.mjs index 045d8f05..31e575a1 100644 --- a/src/core/operations/ParseColourCode.mjs +++ b/src/core/operations/ParseColourCode.mjs @@ -113,7 +113,7 @@ CMYK: ${cmyk} }).on('colorpickerChange', function(e) { var color = e.color.string('rgba'); window.app.manager.input.setInput(color); - window.app.manager.input.debounceInputChange(new Event("keyup")); + window.app.manager.input.inputChange(new Event("keyup")); }); `; } diff --git a/src/web/App.mjs b/src/web/App.mjs index 2d45d1f1..4ead8bc4 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -728,10 +728,11 @@ class App { * @param {event} e */ stateChange(e) { - this.progress = 0; - this.autoBake(); - - this.updateTitle(true, null, true); + debounce(function() { + this.progress = 0; + this.autoBake(); + this.updateTitle(true, null, true); + }, 20, "stateChange", this, [])(); } diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index a46379e9..9d03c728 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -146,7 +146,6 @@ class Manager { this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe); // Input - document.getElementById("input-text").addEventListener("keyup", this.input.debounceInputChange.bind(this.input)); document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app)); this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input); this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input); diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 69417b92..6a1b57df 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -41,24 +41,6 @@ class InputWaiter { this.inputTextEl = document.getElementById("input-text"); this.initEditor(); - // Define keys that don't change the input so we don't have to autobake when they are pressed - this.badKeys = [ - 16, // Shift - 17, // Ctrl - 18, // Alt - 19, // Pause - 20, // Caps - 27, // Esc - 33, 34, 35, 36, // PgUp, PgDn, End, Home - 37, 38, 39, 40, // Directional - 44, // PrntScrn - 91, 92, // Win - 93, // Context - 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, // F1-12 - 144, // Num - 145, // Scroll - ]; - this.inputWorker = null; this.loaderWorkers = []; this.workerId = 0; @@ -125,6 +107,8 @@ class InputWaiter { EditorView.updateListener.of(e => { if (e.selectionSet) this.manager.highlighter.selectionChange("input", e); + if (e.docChanged) + this.inputChange(e); }) ] }); @@ -396,7 +380,7 @@ class InputWaiter { this.showLoadingInfo(r.data, true); break; case "setInput": - debounce(this.set, 50, "setInput", this, [r.data.inputObj, r.data.silent])(); + this.set(r.data.inputObj, r.data.silent); break; case "inputAdded": this.inputAdded(r.data.changeTab, r.data.inputNum); @@ -762,41 +746,30 @@ class InputWaiter { }); } - /** - * Handler for input change events. - * Debounces the input so we don't call autobake too often. - * - * @param {event} e - */ - debounceInputChange(e) { - debounce(this.inputChange, 50, "inputChange", this, [e])(); - } - /** * Handler for input change events. * Updates the value stored in the inputWorker + * Debounces the input so we don't call autobake too often. * * @param {event} e * * @fires Manager#statechange */ inputChange(e) { - // Ignore this function if the input is a file - const fileOverlay = document.getElementById("input-file"); - if (fileOverlay.style.display === "block") return; + debounce(function(e) { + // Ignore this function if the input is a file + const fileOverlay = document.getElementById("input-file"); + if (fileOverlay.style.display === "block") return; - const value = this.getInput(); - const activeTab = this.manager.tabs.getActiveInputTab(); + const value = this.getInput(); + const activeTab = this.manager.tabs.getActiveInputTab(); - this.app.progress = 0; + this.updateInputValue(activeTab, value); + this.manager.tabs.updateInputTabHeader(activeTab, value.slice(0, 100).replace(/[\n\r]/g, "")); - this.updateInputValue(activeTab, value); - this.manager.tabs.updateInputTabHeader(activeTab, value.slice(0, 100).replace(/[\n\r]/g, "")); - - if (e && this.badKeys.indexOf(e.keyCode) < 0) { // Fire the statechange event as the input has been modified window.dispatchEvent(this.manager.statechange); - } + }, 20, "inputChange", this, [e])(); } /** diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index d1fd2532..3f031ac7 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -847,9 +847,7 @@ class OutputWaiter { } } - debounce(this.set, 50, "setOutput", this, [inputNum])(); - - this.outputTextEl.scroll(0, 0); // TODO + this.set(inputNum); if (changeInput) { this.manager.input.changeTab(inputNum, false); From 0dc2322269d4fd26bc6b2aa07f6cb0cd9e3cbce6 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Mon, 11 Jul 2022 13:57:28 +0100 Subject: [PATCH 196/686] Fixed dropping text in the input --- src/web/Manager.mjs | 6 +++--- src/web/waiters/InputWaiter.mjs | 16 ++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 9d03c728..820b1a8d 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -149,9 +149,9 @@ class Manager { document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app)); this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input); this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input); - this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input); - this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input); - this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input); + this.addListeners("#input-wrapper", "dragover", this.input.inputDragover, this.input); + this.addListeners("#input-wrapper", "dragleave", this.input.inputDragleave, this.input); + this.addListeners("#input-wrapper", "drop", this.input.inputDrop, this.input); document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input)); document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input)); document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input)); diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 6a1b57df..ed8f174b 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -797,7 +797,10 @@ class InputWaiter { inputDragleave(e) { e.stopPropagation(); e.preventDefault(); - e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); + // Dragleave often fires when moving between lines in the editor. + // If the target element is within the input-text element, we are still on target. + if (!this.inputTextEl.contains(e.target)) + e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); } /** @@ -813,17 +816,10 @@ class InputWaiter { e.stopPropagation(); e.preventDefault(); - - const text = e.dataTransfer.getData("Text"); - e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); - if (text) { - // Append the text to the current input and fire inputChange() - this.setInput(this.getInput() + text); - this.inputChange(e); - return; - } + // Dropped text is handled by the editor itself + if (e.dataTransfer.getData("Text")) return; if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { this.loadUIFiles(e.dataTransfer.files); From 893b84d0426754d0b21647ef25564f6d9b19f95e Mon Sep 17 00:00:00 2001 From: Luis Martinez Date: Sat, 28 May 2022 00:17:59 -0500 Subject: [PATCH 197/686] xxtea encryption added --- src/core/config/Categories.json | 3 +- src/core/operations/XXTEA.mjs | 182 ++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/XXTEA.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8ac60048..26e56905 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -131,7 +131,8 @@ "Typex", "Lorenz", "Colossus", - "SIGABA" + "SIGABA", + "XXTEA" ] }, { diff --git a/src/core/operations/XXTEA.mjs b/src/core/operations/XXTEA.mjs new file mode 100644 index 00000000..e8264c4d --- /dev/null +++ b/src/core/operations/XXTEA.mjs @@ -0,0 +1,182 @@ +/** + * @author devcydo [devcydo@gmail.com] + * @author Ma Bingyao [mabingyao@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import {toBase64} from "../lib/Base64.mjs"; +import Utils from "../Utils.mjs"; + +/** + * XXTEA Encrypt operation + */ +class XXTEAEncrypt extends Operation { + + /** + * XXTEAEncrypt constructor + */ + constructor() { + super(); + + this.name = "XXTEA"; + this.module = "Default"; + this.description = "Corrected Block TEA (often referred to as XXTEA) is a block cipher designed to correct weaknesses in the original Block TEA. XXTEA operates on variable-length blocks that are some arbitrary multiple of 32 bits in size (minimum 64 bits). The number of full cycles depends on the block size, but there are at least six (rising to 32 for small block sizes). The original Block TEA applies the XTEA round function to each word in the block and combines it additively with its leftmost neighbour. Slow diffusion rate of the decryption process was immediately exploited to break the cipher. Corrected Block TEA uses a more involved round function which makes use of both immediate neighbours in processing each word in the block."; + this.infoURL = "https://wikipedia.org/wiki/XXTEA"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "string", + "value": "", + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let key = args[0]; + + if (input === undefined || input === null || input.length === 0) { + throw new OperationError("Invalid input length (0)"); + } + + if (key === undefined || key === null || key.length === 0) { + throw new OperationError("Invalid key length (0)"); + } + + input = Utils.convertToByteString(input, "utf8"); + key = Utils.convertToByteString(key, "utf8"); + + input = this.convertToUint32Array(input, true); + key = this.fixLength(this.convertToUint32Array(key, false)); + + let encrypted = this.encryptUint32Array(input, key); + + encrypted = toBase64(this.toBinaryString(encrypted, false)); + + return encrypted; + } + + /** + * Convert Uint32Array to binary string + * + * @param {Uint32Array} v + * @param {Boolean} includeLength + * @returns {string} + */ + toBinaryString(v, includeLENGTH) { + const LENGTH = v.LENGTH; + let n = LENGTH << 2; + if (includeLENGTH) { + const M = v[LENGTH - 1]; + n -= 4; + if ((M < n - 3) || (M > n)) { + return null; + } + n = M; + } + for (let i = 0; i < LENGTH; i++) { + v[i] = String.fromCharCode( + v[i] & 0xFF, + v[i] >>> 8 & 0xFF, + v[i] >>> 16 & 0xFF, + v[i] >>> 24 & 0xFF + ); + } + const RESULT = v.join(""); + if (includeLENGTH) { + return RESULT.substring(0, n); + } + return RESULT; + } + + /** + * @param {number} sum + * @param {number} y + * @param {number} z + * @param {number} p + * @param {number} e + * @param {number} k + * @returns {number} + */ + mx(sum, y, z, p, e, k) { + return ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4)) ^ ((sum ^ y) + (k[p & 3 ^ e] ^ z)); + } + + + /** + * Encrypt Uint32Array + * + * @param {Uint32Array} v + * @param {number} k + * @returns {Uint32Array} + */ + encryptUint32Array(v, k) { + const LENGTH = v.LENGTH; + const N = LENGTH - 1; + let y, z, sum, e, p, q; + z = v[N]; + sum = 0; + for (q = Math.floor(6 + 52 / LENGTH) | 0; q > 0; --q) { + sum = (sum + 0x9E3779B9) & 0xFFFFFFFF; + e = sum >>> 2 & 3; + for (p = 0; p < N; ++p) { + y = v[p + 1]; + z = v[p] = (v[p] + this.mx(sum, y, z, p, e, k)) & 0xFFFFFFFF; + } + y = v[0]; + z = v[N] = (v[N] + this.mx(sum, y, z, N, e, k)) & 0xFFFFFFFF; + } + return v; + } + + /** + * Fixes the Uint32Array lenght to 4 + * + * @param {Uint32Array} k + * @returns {Uint32Array} + */ + fixLength(k) { + if (k.length < 4) { + k.length = 4; + } + return k; + } + + /** + * Convert string to Uint32Array + * + * @param {string} bs + * @param {Boolean} includeLength + * @returns {Uint32Array} + */ + convertToUint32Array(bs, includeLength) { + const LENGTH = bs.LENGTH; + let n = LENGTH >> 2; + if ((LENGTH & 3) !== 0) { + ++n; + } + let v; + if (includeLength) { + v = new Array(n + 1); + v[n] = LENGTH; + } else { + v = new Array(n); + } + for (let i = 0; i < LENGTH; ++i) { + v[i >> 2] |= bs.charCodeAt(i) << ((i & 3) << 3); + } + return v; + } + +} + +export default XXTEAEncrypt; From 653af6a3005f872d98ca0d59f0aa118e48f88bb7 Mon Sep 17 00:00:00 2001 From: Luis Martinez Date: Sat, 28 May 2022 00:20:51 -0500 Subject: [PATCH 198/686] xxtea encryption added --- src/core/operations/XXTEA.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/operations/XXTEA.mjs b/src/core/operations/XXTEA.mjs index e8264c4d..4fa0706d 100644 --- a/src/core/operations/XXTEA.mjs +++ b/src/core/operations/XXTEA.mjs @@ -98,7 +98,7 @@ class XXTEAEncrypt extends Operation { return RESULT; } - /** + /** * @param {number} sum * @param {number} y * @param {number} z From c14098a27c47bf345f1f119b970248fd573fa9d6 Mon Sep 17 00:00:00 2001 From: Luis Martinez Date: Mon, 11 Jul 2022 19:38:59 -0500 Subject: [PATCH 199/686] tests added and XXTEA not working correctly fixed --- src/core/operations/XXTEA.mjs | 6 ++-- tests/operations/tests/XXTEA.mjs | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 tests/operations/tests/XXTEA.mjs diff --git a/src/core/operations/XXTEA.mjs b/src/core/operations/XXTEA.mjs index 4fa0706d..1a0a4368 100644 --- a/src/core/operations/XXTEA.mjs +++ b/src/core/operations/XXTEA.mjs @@ -73,7 +73,7 @@ class XXTEAEncrypt extends Operation { * @returns {string} */ toBinaryString(v, includeLENGTH) { - const LENGTH = v.LENGTH; + const LENGTH = v.length; let n = LENGTH << 2; if (includeLENGTH) { const M = v[LENGTH - 1]; @@ -120,7 +120,7 @@ class XXTEAEncrypt extends Operation { * @returns {Uint32Array} */ encryptUint32Array(v, k) { - const LENGTH = v.LENGTH; + const LENGTH = v.length; const N = LENGTH - 1; let y, z, sum, e, p, q; z = v[N]; @@ -159,7 +159,7 @@ class XXTEAEncrypt extends Operation { * @returns {Uint32Array} */ convertToUint32Array(bs, includeLength) { - const LENGTH = bs.LENGTH; + const LENGTH = bs.length; let n = LENGTH >> 2; if ((LENGTH & 3) !== 0) { ++n; diff --git a/tests/operations/tests/XXTEA.mjs b/tests/operations/tests/XXTEA.mjs new file mode 100644 index 00000000..4787f086 --- /dev/null +++ b/tests/operations/tests/XXTEA.mjs @@ -0,0 +1,62 @@ +/** + * Base64 tests. + * + * @author devcydo [devcydo@gmail.com] + * + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "XXTEA", + input: "Hello World! 你好,中国!", + expectedOutput: "QncB1C0rHQoZ1eRiPM4dsZtRi9pNrp7sqvX76cFXvrrIHXL6", + reecipeConfig: [ + { + args: "1234567890" + }, + ], + }, + { + name: "XXTEA", + input: "ნუ პანიკას", + expectedOutput: "PbWjnbFmP8Apu2MKOGNbjeW/72IZLlLMS/g82ozLxwE=", + reecipeConfig: [ + { + args: "1234567890" + }, + ], + }, + { + name: "XXTEA", + input: "ნუ პანიკას", + expectedOutput: "dHrOJ4ClIx6gH33NPSafYR2GG7UqsazY6Xfb0iekBY4=", + reecipeConfig: [ + { + args: "ll3kj209d2" + }, + ], + }, + { + name: "XXTEA", + input: "", + expectedOutput: "Invalid input length (0)", + reecipeConfig: [ + { + args: "1234567890" + }, + ], + }, + { + name: "XXTEA", + input: "", + expectedOutput: "Invalid input length (0)", + reecipeConfig: [ + { + args: "" + }, + ], + }, +]); From e9dd7eceb8e04983085980f913e8ea94b1a11f8d Mon Sep 17 00:00:00 2001 From: john19696 Date: Thu, 14 Jul 2022 14:27:59 +0100 Subject: [PATCH 200/686] upgrade to nodejs v18 --- .nvmrc | 2 +- package-lock.json | 63 ++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/.nvmrc b/.nvmrc index 8e2afd34..3c032078 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -17 \ No newline at end of file +18 diff --git a/package-lock.json b/package-lock.json index e1712692..f174ec5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,7 @@ "babel-loader": "^8.2.5", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "cli-progress": "^3.11.1", "colors": "^1.4.0", "copy-webpack-plugin": "^11.0.0", @@ -3337,12 +3337,27 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/babel-code-frame": { @@ -4496,14 +4511,14 @@ } }, "node_modules/chromedriver": { - "version": "101.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", - "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "version": "103.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-103.0.0.tgz", + "integrity": "sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.2", - "axios": "^0.24.0", + "axios": "^0.27.2", "del": "^6.0.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", @@ -18301,12 +18316,26 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "dev": true, "requires": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "babel-code-frame": { @@ -19208,13 +19237,13 @@ "dev": true }, "chromedriver": { - "version": "101.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", - "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "version": "103.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-103.0.0.tgz", + "integrity": "sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.2", - "axios": "^0.24.0", + "axios": "^0.27.2", "del": "^6.0.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", diff --git a/package.json b/package.json index 48d6f693..46aca7d9 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "babel-loader": "^8.2.5", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-transform-builtin-extend": "1.1.2", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "cli-progress": "^3.11.1", "colors": "^1.4.0", "copy-webpack-plugin": "^11.0.0", From 7c8a185a3d0f48275cad43a9b94a60cbfddc04f6 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Mon, 18 Jul 2022 18:39:41 +0100 Subject: [PATCH 201/686] HTML outputs can now be selected and handle control characters correctly --- src/core/Utils.mjs | 21 ++- src/core/operations/Magic.mjs | 2 +- src/core/operations/ROT13BruteForce.mjs | 6 +- src/core/operations/ROT47BruteForce.mjs | 6 +- .../operations/TextEncodingBruteForce.mjs | 2 +- src/core/operations/ToHexdump.mjs | 29 ++-- src/core/operations/XORBruteForce.mjs | 6 +- src/web/html/index.html | 2 - src/web/stylesheets/layout/_io.css | 21 +-- src/web/stylesheets/utils/_overrides.css | 8 ++ src/web/utils/copyOverride.mjs | 125 ++++++++++++++++++ src/web/utils/editorUtils.mjs | 70 +++++++++- src/web/utils/htmlWidget.mjs | 47 ++++++- src/web/waiters/OptionsWaiter.mjs | 7 - src/web/waiters/OutputWaiter.mjs | 88 +++++------- src/web/waiters/RecipeWaiter.mjs | 3 +- 16 files changed, 319 insertions(+), 124 deletions(-) create mode 100644 src/web/utils/copyOverride.mjs diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 5f36cae9..b72a6028 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -174,17 +174,13 @@ class Utils { * @returns {string} */ static printable(str, preserveWs=false, onlyAscii=false) { - if (isWebEnvironment() && window.app && !window.app.options.treatAsUtf8) { - str = Utils.byteArrayToChars(Utils.strToByteArray(str)); - } - if (onlyAscii) { return str.replace(/[^\x20-\x7f]/g, "."); } // eslint-disable-next-line no-misleading-character-class const re = /[\0-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g; - const wsRe = /[\x09-\x10\x0D\u2028\u2029]/g; + const wsRe = /[\x09-\x10\u2028\u2029]/g; str = str.replace(re, "."); if (!preserveWs) str = str.replace(wsRe, "."); @@ -192,6 +188,21 @@ class Utils { } + /** + * Returns a string with whitespace represented as special characters from the + * Unicode Private Use Area, which CyberChef will display as control characters. + * Private Use Area characters are in the range U+E000..U+F8FF. + * https://en.wikipedia.org/wiki/Private_Use_Areas + * @param {string} str + * @returns {string} + */ + static escapeWhitespace(str) { + return str.replace(/[\x09-\x10]/g, function(c) { + return String.fromCharCode(0xe000 + c.charCodeAt(0)); + }); + } + + /** * Parse a string entered by a user and replace escaped chars with the bytes they represent. * diff --git a/src/core/operations/Magic.mjs b/src/core/operations/Magic.mjs index d5357d95..69cad1db 100644 --- a/src/core/operations/Magic.mjs +++ b/src/core/operations/Magic.mjs @@ -149,7 +149,7 @@ class Magic extends Operation { output += ` ${Utils.generatePrettyRecipe(option.recipe, true)} - ${Utils.escapeHtml(Utils.printable(Utils.truncate(option.data, 99)))} + ${Utils.escapeHtml(Utils.escapeWhitespace(Utils.truncate(option.data, 99)))} ${language}${fileType}${matchingOps}${useful}${validUTF8}${entropy} `; }); diff --git a/src/core/operations/ROT13BruteForce.mjs b/src/core/operations/ROT13BruteForce.mjs index aefe2ab7..7468ee11 100644 --- a/src/core/operations/ROT13BruteForce.mjs +++ b/src/core/operations/ROT13BruteForce.mjs @@ -86,12 +86,12 @@ class ROT13BruteForce extends Operation { } const rotatedString = Utils.byteArrayToUtf8(rotated); if (rotatedString.toLowerCase().indexOf(cribLower) >= 0) { - const rotatedStringPrintable = Utils.printable(rotatedString, false); + const rotatedStringEscaped = Utils.escapeWhitespace(rotatedString); if (printAmount) { const amountStr = "Amount = " + (" " + amount).slice(-2) + ": "; - result.push(amountStr + rotatedStringPrintable); + result.push(amountStr + rotatedStringEscaped); } else { - result.push(rotatedStringPrintable); + result.push(rotatedStringEscaped); } } } diff --git a/src/core/operations/ROT47BruteForce.mjs b/src/core/operations/ROT47BruteForce.mjs index 5f346e00..fa1e90dc 100644 --- a/src/core/operations/ROT47BruteForce.mjs +++ b/src/core/operations/ROT47BruteForce.mjs @@ -66,12 +66,12 @@ class ROT47BruteForce extends Operation { } const rotatedString = Utils.byteArrayToUtf8(rotated); if (rotatedString.toLowerCase().indexOf(cribLower) >= 0) { - const rotatedStringPrintable = Utils.printable(rotatedString, false); + const rotatedStringEscaped = Utils.escapeWhitespace(rotatedString); if (printAmount) { const amountStr = "Amount = " + (" " + amount).slice(-2) + ": "; - result.push(amountStr + rotatedStringPrintable); + result.push(amountStr + rotatedStringEscaped); } else { - result.push(rotatedStringPrintable); + result.push(rotatedStringEscaped); } } } diff --git a/src/core/operations/TextEncodingBruteForce.mjs b/src/core/operations/TextEncodingBruteForce.mjs index 18eb071e..ef8b7f80 100644 --- a/src/core/operations/TextEncodingBruteForce.mjs +++ b/src/core/operations/TextEncodingBruteForce.mjs @@ -79,7 +79,7 @@ class TextEncodingBruteForce extends Operation { let table = ""; for (const enc in encodings) { - const value = Utils.escapeHtml(Utils.printable(encodings[enc], true)); + const value = Utils.escapeHtml(Utils.escapeWhitespace(encodings[enc])); table += ``; } diff --git a/src/core/operations/ToHexdump.mjs b/src/core/operations/ToHexdump.mjs index c657adeb..a52b0451 100644 --- a/src/core/operations/ToHexdump.mjs +++ b/src/core/operations/ToHexdump.mjs @@ -63,33 +63,32 @@ class ToHexdump extends Operation { if (length < 1 || Math.round(length) !== length) throw new OperationError("Width must be a positive integer"); - let output = ""; + const lines = []; for (let i = 0; i < data.length; i += length) { - const buff = data.slice(i, i+length); - let hexa = ""; - for (let j = 0; j < buff.length; j++) { - hexa += Utils.hex(buff[j], padding) + " "; - } - let lineNo = Utils.hex(i, 8); + const buff = data.slice(i, i+length); + const hex = []; + buff.forEach(b => hex.push(Utils.hex(b, padding))); + let hexStr = hex.join(" ").padEnd(length*(padding+1), " "); + + const ascii = Utils.printable(Utils.byteArrayToChars(buff), false, unixFormat); + const asciiStr = ascii.padEnd(buff.length, " "); + if (upperCase) { - hexa = hexa.toUpperCase(); + hexStr = hexStr.toUpperCase(); lineNo = lineNo.toUpperCase(); } - output += lineNo + " " + - hexa.padEnd(length*(padding+1), " ") + - " |" + - Utils.printable(Utils.byteArrayToChars(buff), false, unixFormat).padEnd(buff.length, " ") + - "|\n"; + lines.push(`${lineNo} ${hexStr} |${asciiStr}|`); + if (includeFinalLength && i+buff.length === data.length) { - output += Utils.hex(i+buff.length, 8) + "\n"; + lines.push(Utils.hex(i+buff.length, 8)); } } - return output.slice(0, -1); + return lines.join("\n"); } /** diff --git a/src/core/operations/XORBruteForce.mjs b/src/core/operations/XORBruteForce.mjs index 9b548df8..8c097731 100644 --- a/src/core/operations/XORBruteForce.mjs +++ b/src/core/operations/XORBruteForce.mjs @@ -126,11 +126,7 @@ class XORBruteForce extends Operation { if (crib && resultUtf8.toLowerCase().indexOf(crib) < 0) continue; if (printKey) record += "Key = " + Utils.hex(key, (2*keyLength)) + ": "; - if (outputHex) { - record += toHex(result); - } else { - record += Utils.printable(resultUtf8, false); - } + record += outputHex ? toHex(result) : Utils.escapeWhitespace(resultUtf8); output.push(record); } diff --git a/src/web/html/index.html b/src/web/html/index.html index 3eb150e5..a7931de5 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -264,7 +264,6 @@
    -
    @@ -341,7 +340,6 @@
    -
    diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index cb196709..ea15b6ac 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -177,31 +177,12 @@ } .textarea-wrapper textarea, -.textarea-wrapper #output-text, -.textarea-wrapper #output-highlighter { +.textarea-wrapper #output-text { font-family: var(--fixed-width-font-family); font-size: var(--fixed-width-font-size); color: var(--fixed-width-font-colour); } -#input-highlighter, -#output-highlighter { - position: absolute; - left: 0; - bottom: 0; - width: 100%; - padding: 3px; - margin: 0; - overflow: hidden; - letter-spacing: normal; - white-space: pre-wrap; - word-wrap: break-word; - color: #fff; - background-color: transparent; - border: none; - pointer-events: none; -} - #output-loader { position: absolute; bottom: 0; diff --git a/src/web/stylesheets/utils/_overrides.css b/src/web/stylesheets/utils/_overrides.css index fa216836..920aab89 100755 --- a/src/web/stylesheets/utils/_overrides.css +++ b/src/web/stylesheets/utils/_overrides.css @@ -232,3 +232,11 @@ optgroup { .colorpicker-color div { height: 100px; } + + +/* CodeMirror */ + +.ͼ2 .cm-specialChar, +.cm-specialChar { + color: red; +} diff --git a/src/web/utils/copyOverride.mjs b/src/web/utils/copyOverride.mjs new file mode 100644 index 00000000..51b2386b --- /dev/null +++ b/src/web/utils/copyOverride.mjs @@ -0,0 +1,125 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + * + * In order to render whitespace characters as control character pictures in the output, even + * when they are the designated line separator, CyberChef sometimes chooses to represent them + * internally using the Unicode Private Use Area (https://en.wikipedia.org/wiki/Private_Use_Areas). + * See `Utils.escapeWhitespace()` for an example of this. + * + * The `renderSpecialChar()` function understands that it should display these characters as + * control pictures. When copying data from the Output, we need to replace these PUA characters + * with their original values, so we override the DOM "copy" event and modify the copied data + * if required. This handler is based closely on the built-in CodeMirror handler and defers to the + * built-in handler if PUA characters are not present in the copied data, in order to minimise the + * impact of breaking changes. + */ + +import {EditorView} from "@codemirror/view"; + +/** + * Copies the currently selected text from the state doc. + * Based on the built-in implementation with a few unrequired bits taken out: + * https://github.com/codemirror/view/blob/7d9c3e54396242d17b3164a0e244dcc234ee50ee/src/input.ts#L604 + * + * @param {EditorState} state + * @returns {Object} + */ +function copiedRange(state) { + const content = []; + let linewise = false; + for (const range of state.selection.ranges) if (!range.empty) { + content.push(state.sliceDoc(range.from, range.to)); + } + if (!content.length) { + // Nothing selected, do a line-wise copy + let upto = -1; + for (const {from} of state.selection.ranges) { + const line = state.doc.lineAt(from); + if (line.number > upto) { + content.push(line.text); + } + upto = line.number; + } + linewise = true; + } + + return {text: content.join(state.lineBreak), linewise}; +} + +/** + * Regex to match characters in the Private Use Area of the Unicode table. + */ +const PUARegex = new RegExp("[\ue000-\uf8ff]"); +const PUARegexG = new RegExp("[\ue000-\uf8ff]", "g"); +/** + * Regex tto match Unicode Control Pictures. + */ +const CPRegex = new RegExp("[\u2400-\u243f]"); +const CPRegexG = new RegExp("[\u2400-\u243f]", "g"); + +/** + * Overrides the DOM "copy" handler in the CodeMirror editor in order to return the original + * values of control characters that have been represented in the Unicode Private Use Area for + * visual purposes. + * Based on the built-in copy handler with some modifications: + * https://github.com/codemirror/view/blob/7d9c3e54396242d17b3164a0e244dcc234ee50ee/src/input.ts#L629 + * + * This handler will defer to the built-in version if no PUA characters are present. + * + * @returns {Extension} + */ +export function copyOverride() { + return EditorView.domEventHandlers({ + copy(event, view) { + const {text, linewise} = copiedRange(view.state); + if (!text && !linewise) return; + + // If there are no PUA chars in the copied text, return false and allow the built-in + // copy handler to fire + if (!PUARegex.test(text)) return false; + + // If PUA chars are detected, modify them back to their original values and copy that instead + const rawText = text.replace(PUARegexG, function(c) { + return String.fromCharCode(c.charCodeAt(0) - 0xe000); + }); + + event.preventDefault(); + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", rawText); + + // Returning true prevents CodeMirror default handlers from firing + return true; + } + }); +} + + +/** + * Handler for copy events in output-html decorations. If there are control pictures present, + * this handler will convert them back to their raw form before copying. If there are no + * control pictures present, it will do nothing and defer to the default browser handler. + * + * @param {ClipboardEvent} event + * @returns {boolean} + */ +export function htmlCopyOverride(event) { + const text = window.getSelection().toString(); + if (!text) return; + + // If there are no control picture chars in the copied text, return false and allow the built-in + // copy handler to fire + if (!CPRegex.test(text)) return false; + + // If control picture chars are detected, modify them back to their original values and copy that instead + const rawText = text.replace(CPRegexG, function(c) { + return String.fromCharCode(c.charCodeAt(0) - 0x2400); + }); + + event.preventDefault(); + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", rawText); + + return true; +} diff --git a/src/web/utils/editorUtils.mjs b/src/web/utils/editorUtils.mjs index fe6b83d4..cb0ebed1 100644 --- a/src/web/utils/editorUtils.mjs +++ b/src/web/utils/editorUtils.mjs @@ -6,12 +6,41 @@ * @license Apache-2.0 */ +import Utils from "../../core/Utils.mjs"; + +// Descriptions for named control characters +const Names = { + 0: "null", + 7: "bell", + 8: "backspace", + 10: "line feed", + 11: "vertical tab", + 13: "carriage return", + 27: "escape", + 8203: "zero width space", + 8204: "zero width non-joiner", + 8205: "zero width joiner", + 8206: "left-to-right mark", + 8207: "right-to-left mark", + 8232: "line separator", + 8237: "left-to-right override", + 8238: "right-to-left override", + 8233: "paragraph separator", + 65279: "zero width no-break space", + 65532: "object replacement" +}; + +// Regex for Special Characters to be replaced +const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g"; +const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc\ue000-\uf8ff]", UnicodeRegexpSupport); + /** * Override for rendering special characters. * Should mirror the toDOM function in * https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150 * But reverts the replacement of line feeds with newline control pictures. + * * @param {number} code * @param {string} desc * @param {string} placeholder @@ -19,10 +48,47 @@ */ export function renderSpecialChar(code, desc, placeholder) { const s = document.createElement("span"); - // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back. - s.textContent = code === 0x0a ? "\u240a" : placeholder; + + // CodeMirror changes 0x0a to "NL" instead of "LF". We change it back along with its description. + if (code === 0x0a) { + placeholder = "\u240a"; + desc = desc.replace("newline", "line feed"); + } + + // Render CyberChef escaped characters correctly - see Utils.escapeWhitespace + if (code >= 0xe000 && code <= 0xf8ff) { + code = code - 0xe000; + placeholder = String.fromCharCode(0x2400 + code); + desc = "Control character " + (Names[code] || "0x" + code.toString(16)); + } + + s.textContent = placeholder; s.title = desc; s.setAttribute("aria-label", desc); s.className = "cm-specialChar"; return s; } + + +/** + * Given a string, returns that string with any control characters replaced with HTML + * renderings of control pictures. + * + * @param {string} str + * @param {boolean} [preserveWs=false] + * @param {string} [lineBreak="\n"] + * @returns {html} + */ +export function escapeControlChars(str, preserveWs=false, lineBreak="\n") { + if (!preserveWs) + str = Utils.escapeWhitespace(str); + + return str.replace(Specials, function(c) { + if (lineBreak.includes(c)) return c; + const code = c.charCodeAt(0); + const desc = "Control character " + (Names[code] || "0x" + code.toString(16)); + const placeholder = code > 32 ? "\u2022" : String.fromCharCode(9216 + code); + const n = renderSpecialChar(code, desc, placeholder); + return n.outerHTML; + }); +} diff --git a/src/web/utils/htmlWidget.mjs b/src/web/utils/htmlWidget.mjs index fbce9b49..5e5c41c1 100644 --- a/src/web/utils/htmlWidget.mjs +++ b/src/web/utils/htmlWidget.mjs @@ -5,6 +5,9 @@ */ import {WidgetType, Decoration, ViewPlugin} from "@codemirror/view"; +import {escapeControlChars} from "./editorUtils.mjs"; +import {htmlCopyOverride} from "./copyOverride.mjs"; + /** * Adds an HTML widget to the Code Mirror editor @@ -14,9 +17,10 @@ class HTMLWidget extends WidgetType { /** * HTMLWidget consructor */ - constructor(html) { + constructor(html, view) { super(); this.html = html; + this.view = view; } /** @@ -27,9 +31,45 @@ class HTMLWidget extends WidgetType { const wrap = document.createElement("span"); wrap.setAttribute("id", "output-html"); wrap.innerHTML = this.html; + + // Find text nodes and replace unprintable chars with control codes + this.walkTextNodes(wrap); + + // Add a handler for copy events to ensure the control codes are copied correctly + wrap.addEventListener("copy", htmlCopyOverride); return wrap; } + /** + * Walks all text nodes in a given element + * @param {DOMNode} el + */ + walkTextNodes(el) { + for (const node of el.childNodes) { + switch (node.nodeType) { + case Node.TEXT_NODE: + this.replaceControlChars(node); + break; + default: + if (node.nodeName !== "SCRIPT" && + node.nodeName !== "STYLE") + this.walkTextNodes(node); + break; + } + } + } + + /** + * Renders control characters in text nodes + * @param {DOMNode} textNode + */ + replaceControlChars(textNode) { + const val = escapeControlChars(textNode.nodeValue, true, this.view.state.lineBreak); + const node = document.createElement("null"); + node.innerHTML = val; + textNode.parentNode.replaceChild(node, textNode); + } + } /** @@ -42,7 +82,7 @@ function decorateHTML(view, html) { const widgets = []; if (html.length) { const deco = Decoration.widget({ - widget: new HTMLWidget(html), + widget: new HTMLWidget(html, view), side: 1 }); widgets.push(deco.range(0)); @@ -79,7 +119,8 @@ export function htmlPlugin(htmlOutput) { } } }, { - decorations: v => v.decorations + decorations: v => v.decorations, + } ); diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs index 7d9a3e2d..36beef7e 100755 --- a/src/web/waiters/OptionsWaiter.mjs +++ b/src/web/waiters/OptionsWaiter.mjs @@ -141,13 +141,6 @@ class OptionsWaiter { setWordWrap() { this.manager.input.setWordWrap(this.app.options.wordWrap); this.manager.output.setWordWrap(this.app.options.wordWrap); - document.getElementById("input-highlighter").classList.remove("word-wrap"); - document.getElementById("output-highlighter").classList.remove("word-wrap"); - - if (!this.app.options.wordWrap) { - document.getElementById("input-highlighter").classList.add("word-wrap"); - document.getElementById("output-highlighter").classList.add("word-wrap"); - } } diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index 3f031ac7..deaeaed3 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -5,7 +5,7 @@ * @license Apache-2.0 */ -import Utils, { debounce } from "../../core/Utils.mjs"; +import Utils, {debounce} from "../../core/Utils.mjs"; import Dish from "../../core/Dish.mjs"; import FileSaver from "file-saver"; import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs"; @@ -19,8 +19,9 @@ import {bracketMatching} from "@codemirror/language"; import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search"; import {statusBar} from "../utils/statusBar.mjs"; -import {renderSpecialChar} from "../utils/editorUtils.mjs"; import {htmlPlugin} from "../utils/htmlWidget.mjs"; +import {copyOverride} from "../utils/copyOverride.mjs"; +import {renderSpecialChar} from "../utils/editorUtils.mjs"; /** * Waiter to handle events related to the output @@ -61,7 +62,8 @@ class OutputWaiter { initEditor() { this.outputEditorConf = { eol: new Compartment, - lineWrapping: new Compartment + lineWrapping: new Compartment, + drawSelection: new Compartment }; const initialState = EditorState.create({ @@ -69,9 +71,10 @@ class OutputWaiter { extensions: [ // Editor extensions EditorState.readOnly.of(true), - htmlPlugin(this.htmlOutput), - highlightSpecialChars({render: renderSpecialChar}), - drawSelection(), + highlightSpecialChars({ + render: renderSpecialChar, // Custom character renderer to handle special cases + addSpecialChars: /[\ue000-\uf8ff]/g // Add the Unicode Private Use Area which we use for some whitespace chars + }), rectangularSelection(), crosshairCursor(), bracketMatching(), @@ -79,16 +82,19 @@ class OutputWaiter { search({top: true}), EditorState.allowMultipleSelections.of(true), - // Custom extensiosn + // Custom extensions statusBar({ label: "Output", bakeStats: this.bakeStats, eolHandler: this.eolChange.bind(this) }), + htmlPlugin(this.htmlOutput), + copyOverride(), // Mutable state this.outputEditorConf.lineWrapping.of(EditorView.lineWrapping), this.outputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), + this.outputEditorConf.drawSelection.of(drawSelection()), // Keymap keymap.of([ @@ -153,6 +159,14 @@ class OutputWaiter { * @param {string} data */ setOutput(data) { + // Turn drawSelection back on + this.outputEditorView.dispatch({ + effects: this.outputEditorConf.drawSelection.reconfigure( + drawSelection() + ) + }); + + // Insert data into editor this.outputEditorView.dispatch({ changes: { from: 0, @@ -173,6 +187,11 @@ class OutputWaiter { // triggers the htmlWidget to render the HTML. this.setOutput(""); + // Turn off drawSelection + this.outputEditorView.dispatch({ + effects: this.outputEditorConf.drawSelection.reconfigure([]) + }); + // Execute script sections const scriptElements = document.getElementById("output-html").querySelectorAll("script"); for (let i = 0; i < scriptElements.length; i++) { @@ -414,8 +433,6 @@ class OutputWaiter { if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10); const outputFile = document.getElementById("output-file"); - const outputHighlighter = document.getElementById("output-highlighter"); - const inputHighlighter = document.getElementById("input-highlighter"); // If pending or baking, show loader and status message // If error, style the tab and handle the error @@ -447,8 +464,6 @@ class OutputWaiter { this.outputTextEl.style.display = "block"; this.outputTextEl.classList.remove("blur"); outputFile.style.display = "none"; - outputHighlighter.display = "none"; - inputHighlighter.display = "none"; this.clearHTMLOutput(); if (output.error) { @@ -463,8 +478,6 @@ class OutputWaiter { if (output.data === null) { this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; - outputHighlighter.display = "block"; - inputHighlighter.display = "block"; this.clearHTMLOutput(); this.setOutput(""); @@ -478,15 +491,11 @@ class OutputWaiter { switch (output.data.type) { case "html": outputFile.style.display = "none"; - outputHighlighter.style.display = "none"; - inputHighlighter.style.display = "none"; this.setHTMLOutput(output.data.result); break; case "ArrayBuffer": this.outputTextEl.style.display = "block"; - outputHighlighter.display = "none"; - inputHighlighter.display = "none"; this.clearHTMLOutput(); this.setOutput(""); @@ -497,8 +506,6 @@ class OutputWaiter { default: this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; - outputHighlighter.display = "block"; - inputHighlighter.display = "block"; this.clearHTMLOutput(); this.setOutput(output.data.result); @@ -1215,8 +1222,6 @@ class OutputWaiter { document.querySelector("#output-loader .loading-msg").textContent = "Loading file slice..."; this.toggleLoader(true); const outputFile = document.getElementById("output-file"), - outputHighlighter = document.getElementById("output-highlighter"), - inputHighlighter = document.getElementById("input-highlighter"), showFileOverlay = document.getElementById("show-file-overlay"), sliceFromEl = document.getElementById("output-file-slice-from"), sliceToEl = document.getElementById("output-file-slice-to"), @@ -1238,8 +1243,6 @@ class OutputWaiter { this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; - outputHighlighter.display = "block"; - inputHighlighter.display = "block"; this.toggleLoader(false); } @@ -1251,8 +1254,6 @@ class OutputWaiter { document.querySelector("#output-loader .loading-msg").textContent = "Loading entire file at user instruction. This may cause a crash..."; this.toggleLoader(true); const outputFile = document.getElementById("output-file"), - outputHighlighter = document.getElementById("output-highlighter"), - inputHighlighter = document.getElementById("input-highlighter"), showFileOverlay = document.getElementById("show-file-overlay"), output = this.outputs[this.manager.tabs.getActiveOutputTab()].data; @@ -1270,8 +1271,6 @@ class OutputWaiter { this.outputTextEl.style.display = "block"; outputFile.style.display = "none"; - outputHighlighter.display = "block"; - inputHighlighter.display = "block"; this.toggleLoader(false); } @@ -1319,36 +1318,13 @@ class OutputWaiter { } const output = await dish.get(Dish.STRING); + const self = this; - // Create invisible textarea to populate with the raw dish string (not the printable version that - // contains dots instead of the actual bytes) - const textarea = document.createElement("textarea"); - textarea.style.position = "fixed"; - textarea.style.top = 0; - textarea.style.left = 0; - textarea.style.width = 0; - textarea.style.height = 0; - textarea.style.border = "none"; - - textarea.value = output; - document.body.appendChild(textarea); - - let success = false; - try { - textarea.select(); - success = output && document.execCommand("copy"); - } catch (err) { - success = false; - } - - if (success) { - this.app.alert("Copied raw output successfully.", 2000); - } else { - this.app.alert("Sorry, the output could not be copied.", 3000); - } - - // Clean up - document.body.removeChild(textarea); + navigator.clipboard.writeText(output).then(function() { + self.app.alert("Copied raw output successfully.", 2000); + }, function(err) { + self.app.alert("Sorry, the output could not be copied.", 3000); + }); } /** diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index f4107e66..d907a67c 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -7,6 +7,7 @@ import HTMLOperation from "../HTMLOperation.mjs"; import Sortable from "sortablejs"; import Utils from "../../core/Utils.mjs"; +import {escapeControlChars} from "../utils/editorUtils.mjs"; /** @@ -568,7 +569,7 @@ class RecipeWaiter { const registerList = []; for (let i = 0; i < registers.length; i++) { - registerList.push(`$R${numPrevRegisters + i} = ${Utils.escapeHtml(Utils.truncate(Utils.printable(registers[i]), 100))}`); + registerList.push(`$R${numPrevRegisters + i} = ${escapeControlChars(Utils.escapeHtml(Utils.truncate(registers[i], 100)))}`); } const registerListEl = `
    ${registerList.join("
    ")} From 2f89130f41d195cd86bfc2e7bc85036dc66cb7eb Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Thu, 21 Jul 2022 16:36:15 +0200 Subject: [PATCH 202/686] fix protobuf field order --- src/core/lib/Protobuf.mjs | 2 +- tests/operations/tests/Protobuf.mjs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/lib/Protobuf.mjs b/src/core/lib/Protobuf.mjs index 135933ca..e131d3a5 100644 --- a/src/core/lib/Protobuf.mjs +++ b/src/core/lib/Protobuf.mjs @@ -184,7 +184,7 @@ class Protobuf { bytes: String, longs: Number, enums: String, - defualts: true + defaults: true }); const output = {}; diff --git a/tests/operations/tests/Protobuf.mjs b/tests/operations/tests/Protobuf.mjs index 17adfd88..2131e723 100644 --- a/tests/operations/tests/Protobuf.mjs +++ b/tests/operations/tests/Protobuf.mjs @@ -40,10 +40,10 @@ TestRegister.addTests([ "Apple": [ 28 ], - "Banana": "You", "Carrot": [ "Me" - ] + ], + "Banana": "You" }, null, 4), recipeConfig: [ { @@ -72,10 +72,10 @@ TestRegister.addTests([ "Apple": [ 28 ], - "Banana": "You", "Carrot": [ "Me" - ] + ], + "Banana": "You" }, "Unknown Fields": { "4": 43, @@ -111,10 +111,10 @@ TestRegister.addTests([ "Apple": [ 28 ], - "Banana": "You", "Carrot": [ "Me" ], + "Banana": "You", "Date": 43, "Elderberry": { "Fig": "abc123", @@ -154,10 +154,10 @@ TestRegister.addTests([ input: "0d1c0000001203596f751a024d65202b2a0a0a06616263313233120031ba32a96cc10200003801", expectedOutput: JSON.stringify({ "Test": { - "Banana (string)": "You", "Carrot (string)": [ "Me" ], + "Banana (string)": "You", "Date (int32)": 43, "Imbe (Options)": "Option1" }, From 475282984bda96535fb7d41c9d61d561a1c5b720 Mon Sep 17 00:00:00 2001 From: Philippe Arteau Date: Fri, 29 Jul 2022 14:32:46 -0400 Subject: [PATCH 203/686] Minor typos --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 07257ede..021e3515 100755 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ You can use as many operations as you like in simple or complex ways. Some examp - Whenever you modify the input or the recipe, CyberChef will automatically "bake" for you and produce the output immediately. - This can be turned off and operated manually if it is affecting performance (if the input is very large, for instance). - Automated encoding detection - - CyberChef uses [a number of techniques](https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic) to attempt to automatically detect which encodings your data is under. If it finds a suitable operation which can make sense of your data, it displays the 'magic' icon in the Output field which you can click to decode your data. + - CyberChef uses [a number of techniques](https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic) to attempt to automatically detect which encodings your data is under. If it finds a suitable operation that make sense of your data, it displays the 'magic' icon in the Output field which you can click to decode your data. - Breakpoints - You can set breakpoints on any operation in your recipe to pause execution before running it. - You can also step through the recipe one operation at a time to see what the data looks like at each stage. @@ -66,7 +66,7 @@ You can use as many operations as you like in simple or complex ways. Some examp - Highlighting - When you highlight text in the input or output, the offset and length values will be displayed and, if possible, the corresponding data will be highlighted in the output or input respectively (example: [highlight the word 'question' in the input to see where it appears in the output][11]). - Save to file and load from file - - You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 2GB are supported (depending on your browser), however some operations may take a very long time to run over this much data. + - You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 2GB are supported (depending on your browser), however, some operations may take a very long time to run over this much data. - CyberChef is entirely client-side - It should be noted that none of your recipe configuration or input (either text or files) is ever sent to the CyberChef web server - all processing is carried out within your browser, on your own computer. - Due to this feature, CyberChef can be downloaded and run locally. You can use the link in the top left corner of the app to download a full copy of CyberChef and drop it into a virtual machine, share it with other people, or host it in a closed network. @@ -74,7 +74,7 @@ You can use as many operations as you like in simple or complex ways. Some examp ## Deep linking -By manipulation of CyberChef's URL hash, you can change the initial settings with which the page opens. +By manipulating CyberChef's URL hash, you can change the initial settings with which the page opens. The format is `https://gchq.github.io/CyberChef/#recipe=Operation()&input=...` Supported arguments are `recipe`, `input` (encoded in Base64), and `theme`. @@ -90,12 +90,12 @@ CyberChef is built to support ## Node.js support -CyberChef is built to fully support Node.js `v10` and partially supports `v12`. Named imports using a deep import specifier does not work in `v12`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki) +CyberChef is built to fully support Node.js `v10` and partially supports `v12`. Named imports using a deep import specifier do not work in `v12`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki) ## Contributing -Contributing a new operation to CyberChef is super easy! There is a quickstart script which will walk you through the process. If you can write basic JavaScript, you can write a CyberChef operation. +Contributing a new operation to CyberChef is super easy! The quickstart script will walk you through the process. If you can write basic JavaScript, you can write a CyberChef operation. An installation walkthrough, how-to guides for adding new operations and themes, descriptions of the repository structure, available data types and coding conventions can all be found in the project [wiki pages](https://github.com/gchq/CyberChef/wiki). From 69e59916e25be3fe8511ca8134df2e0b444de166 Mon Sep 17 00:00:00 2001 From: jeiea Date: Wed, 17 Aug 2022 02:12:39 +0900 Subject: [PATCH 204/686] feat: support boolean and null in JSON to CSV --- src/core/operations/JSONToCSV.mjs | 7 +++++-- tests/operations/tests/JSONtoCSV.mjs | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/core/operations/JSONToCSV.mjs b/src/core/operations/JSONToCSV.mjs index 7eb3e3b4..875ff6e8 100644 --- a/src/core/operations/JSONToCSV.mjs +++ b/src/core/operations/JSONToCSV.mjs @@ -114,8 +114,11 @@ class JSONToCSV extends Operation { * @returns {string} */ escapeCellContents(data, force=false) { - if (typeof data === "number") data = data.toString(); - if (force && typeof data !== "string") data = JSON.stringify(data); + if (data !== "string") { + const isPrimitive = data == null || typeof data !== "object"; + if (isPrimitive) data = `${data}`; + else if (force) data = JSON.stringify(data); + } // Double quotes should be doubled up data = data.replace(/"/g, '""'); diff --git a/tests/operations/tests/JSONtoCSV.mjs b/tests/operations/tests/JSONtoCSV.mjs index a9a0867e..faf373d1 100644 --- a/tests/operations/tests/JSONtoCSV.mjs +++ b/tests/operations/tests/JSONtoCSV.mjs @@ -46,6 +46,17 @@ TestRegister.addTests([ }, ], }, + { + name: "JSON to CSV: boolean and null as values", + input: JSON.stringify({a: false, b: null, c: 3}), + expectedOutput: "a,b,c\r\nfalse,null,3\r\n", + recipeConfig: [ + { + op: "JSON to CSV", + args: [",", "\\r\\n"] + }, + ], + }, { name: "JSON to CSV: JSON as an array", input: JSON.stringify([{a: 1, b: "2", c: 3}]), From e93aa42697b5101791b2bc1238f8b687c08cf84f Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 2 Sep 2022 12:56:04 +0100 Subject: [PATCH 205/686] Input and output character encodings can now be set --- src/core/Chef.mjs | 8 +- src/core/ChefWorker.js | 7 +- src/core/Utils.mjs | 8 + src/core/lib/ChrEnc.mjs | 13 +- src/core/operations/DecodeText.mjs | 8 +- src/core/operations/EncodeText.mjs | 8 +- .../operations/TextEncodingBruteForce.mjs | 10 +- src/web/Manager.mjs | 1 - src/web/html/index.html | 3 - src/web/stylesheets/layout/_io.css | 42 ++- src/web/utils/htmlWidget.mjs | 11 +- src/web/utils/statusBar.mjs | 231 +++++++++++++--- src/web/waiters/InputWaiter.mjs | 168 +++++++----- src/web/waiters/OutputWaiter.mjs | 132 ++++----- src/web/workers/InputWorker.mjs | 255 +++++------------- 15 files changed, 482 insertions(+), 423 deletions(-) diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index 36998cec..140774bc 100755 --- a/src/core/Chef.mjs +++ b/src/core/Chef.mjs @@ -68,16 +68,10 @@ class Chef { // Present the raw result await recipe.present(this.dish); - // Depending on the size of the output, we may send it back as a string or an ArrayBuffer. - // This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file. - // The threshold is specified in KiB. - const threshold = (options.ioDisplayThreshold || 1024) * 1024; const returnType = this.dish.type === Dish.HTML ? Dish.HTML : - this.dish.size > threshold ? - Dish.ARRAY_BUFFER : - Dish.STRING; + Dish.ARRAY_BUFFER; return { dish: rawDish, diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index d46a705d..8989875a 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -101,14 +101,17 @@ async function bake(data) { // Ensure the relevant modules are loaded self.loadRequiredModules(data.recipeConfig); try { - self.inputNum = (data.inputNum !== undefined) ? data.inputNum : -1; + self.inputNum = data.inputNum === undefined ? -1 : data.inputNum; const response = await self.chef.bake( data.input, // The user's input data.recipeConfig, // The configuration of the recipe data.options // Options set by the user ); - const transferable = (data.input instanceof ArrayBuffer) ? [data.input] : undefined; + const transferable = (response.dish.value instanceof ArrayBuffer) ? + [response.dish.value] : + undefined; + self.postMessage({ action: "bakeComplete", data: Object.assign(response, { diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index b72a6028..604b7b8c 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -406,6 +406,7 @@ class Utils { * Utils.strToArrayBuffer("你好"); */ static strToArrayBuffer(str) { + log.debug("Converting string to array buffer"); const arr = new Uint8Array(str.length); let i = str.length, b; while (i--) { @@ -432,6 +433,7 @@ class Utils { * Utils.strToUtf8ArrayBuffer("你好"); */ static strToUtf8ArrayBuffer(str) { + log.debug("Converting string to UTF8 array buffer"); const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { @@ -461,6 +463,7 @@ class Utils { * Utils.strToByteArray("你好"); */ static strToByteArray(str) { + log.debug("Converting string to byte array"); const byteArray = new Array(str.length); let i = str.length, b; while (i--) { @@ -487,6 +490,7 @@ class Utils { * Utils.strToUtf8ByteArray("你好"); */ static strToUtf8ByteArray(str) { + log.debug("Converting string to UTF8 byte array"); const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { @@ -515,6 +519,7 @@ class Utils { * Utils.strToCharcode("你好"); */ static strToCharcode(str) { + log.debug("Converting string to charcode"); const charcode = []; for (let i = 0; i < str.length; i++) { @@ -549,6 +554,7 @@ class Utils { * Utils.byteArrayToUtf8([228,189,160,229,165,189]); */ static byteArrayToUtf8(byteArray) { + log.debug("Converting byte array to UTF8"); const str = Utils.byteArrayToChars(byteArray); try { const utf8Str = utf8.decode(str); @@ -581,6 +587,7 @@ class Utils { * Utils.byteArrayToChars([20320,22909]); */ static byteArrayToChars(byteArray) { + log.debug("Converting byte array to chars"); if (!byteArray) return ""; let str = ""; // String concatenation appears to be faster than an array join @@ -603,6 +610,7 @@ class Utils { * Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer); */ static arrayBufferToStr(arrayBuffer, utf8=true) { + log.debug("Converting array buffer to str"); const arr = new Uint8Array(arrayBuffer); return utf8 ? Utils.byteArrayToUtf8(arr) : Utils.byteArrayToChars(arr); } diff --git a/src/core/lib/ChrEnc.mjs b/src/core/lib/ChrEnc.mjs index c5cb5605..8934d137 100644 --- a/src/core/lib/ChrEnc.mjs +++ b/src/core/lib/ChrEnc.mjs @@ -9,7 +9,7 @@ /** * Character encoding format mappings. */ -export const IO_FORMAT = { +export const CHR_ENC_CODE_PAGES = { "UTF-8 (65001)": 65001, "UTF-7 (65000)": 65000, "UTF-16LE (1200)": 1200, @@ -164,6 +164,17 @@ export const IO_FORMAT = { "Simplified Chinese GB18030 (54936)": 54936, }; + +export const CHR_ENC_SIMPLE_LOOKUP = {}; +export const CHR_ENC_SIMPLE_REVERSE_LOOKUP = {}; + +for (const name in CHR_ENC_CODE_PAGES) { + const simpleName = name.match(/(^.+)\([\d/]+\)$/)[1]; + + CHR_ENC_SIMPLE_LOOKUP[simpleName] = CHR_ENC_CODE_PAGES[name]; + CHR_ENC_SIMPLE_REVERSE_LOOKUP[CHR_ENC_CODE_PAGES[name]] = simpleName; +} + /** * Unicode Normalisation Forms * diff --git a/src/core/operations/DecodeText.mjs b/src/core/operations/DecodeText.mjs index 9b01b79f..0fc9d2b5 100644 --- a/src/core/operations/DecodeText.mjs +++ b/src/core/operations/DecodeText.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation.mjs"; import cptable from "codepage"; -import {IO_FORMAT} from "../lib/ChrEnc.mjs"; +import {CHR_ENC_CODE_PAGES} from "../lib/ChrEnc.mjs"; /** * Decode text operation @@ -26,7 +26,7 @@ class DecodeText extends Operation { "

    ", "Supported charsets are:", "
      ", - Object.keys(IO_FORMAT).map(e => `
    • ${e}
    • `).join("\n"), + Object.keys(CHR_ENC_CODE_PAGES).map(e => `
    • ${e}
    • `).join("\n"), "
    ", ].join("\n"); this.infoURL = "https://wikipedia.org/wiki/Character_encoding"; @@ -36,7 +36,7 @@ class DecodeText extends Operation { { "name": "Encoding", "type": "option", - "value": Object.keys(IO_FORMAT) + "value": Object.keys(CHR_ENC_CODE_PAGES) } ]; } @@ -47,7 +47,7 @@ class DecodeText extends Operation { * @returns {string} */ run(input, args) { - const format = IO_FORMAT[args[0]]; + const format = CHR_ENC_CODE_PAGES[args[0]]; return cptable.utils.decode(format, new Uint8Array(input)); } diff --git a/src/core/operations/EncodeText.mjs b/src/core/operations/EncodeText.mjs index 8fc61fce..8cc1450f 100644 --- a/src/core/operations/EncodeText.mjs +++ b/src/core/operations/EncodeText.mjs @@ -6,7 +6,7 @@ import Operation from "../Operation.mjs"; import cptable from "codepage"; -import {IO_FORMAT} from "../lib/ChrEnc.mjs"; +import {CHR_ENC_CODE_PAGES} from "../lib/ChrEnc.mjs"; /** * Encode text operation @@ -26,7 +26,7 @@ class EncodeText extends Operation { "

    ", "Supported charsets are:", "
      ", - Object.keys(IO_FORMAT).map(e => `
    • ${e}
    • `).join("\n"), + Object.keys(CHR_ENC_CODE_PAGES).map(e => `
    • ${e}
    • `).join("\n"), "
    ", ].join("\n"); this.infoURL = "https://wikipedia.org/wiki/Character_encoding"; @@ -36,7 +36,7 @@ class EncodeText extends Operation { { "name": "Encoding", "type": "option", - "value": Object.keys(IO_FORMAT) + "value": Object.keys(CHR_ENC_CODE_PAGES) } ]; } @@ -47,7 +47,7 @@ class EncodeText extends Operation { * @returns {ArrayBuffer} */ run(input, args) { - const format = IO_FORMAT[args[0]]; + const format = CHR_ENC_CODE_PAGES[args[0]]; const encoded = cptable.utils.encode(format, input); return new Uint8Array(encoded).buffer; } diff --git a/src/core/operations/TextEncodingBruteForce.mjs b/src/core/operations/TextEncodingBruteForce.mjs index ef8b7f80..ae96fd0a 100644 --- a/src/core/operations/TextEncodingBruteForce.mjs +++ b/src/core/operations/TextEncodingBruteForce.mjs @@ -8,7 +8,7 @@ import Operation from "../Operation.mjs"; import Utils from "../Utils.mjs"; import cptable from "codepage"; -import {IO_FORMAT} from "../lib/ChrEnc.mjs"; +import {CHR_ENC_CODE_PAGES} from "../lib/ChrEnc.mjs"; /** * Text Encoding Brute Force operation @@ -28,7 +28,7 @@ class TextEncodingBruteForce extends Operation { "

    ", "Supported charsets are:", "
      ", - Object.keys(IO_FORMAT).map(e => `
    • ${e}
    • `).join("\n"), + Object.keys(CHR_ENC_CODE_PAGES).map(e => `
    • ${e}
    • `).join("\n"), "
    " ].join("\n"); this.infoURL = "https://wikipedia.org/wiki/Character_encoding"; @@ -51,15 +51,15 @@ class TextEncodingBruteForce extends Operation { */ run(input, args) { const output = {}, - charsets = Object.keys(IO_FORMAT), + charsets = Object.keys(CHR_ENC_CODE_PAGES), mode = args[0]; charsets.forEach(charset => { try { if (mode === "Decode") { - output[charset] = cptable.utils.decode(IO_FORMAT[charset], input); + output[charset] = cptable.utils.decode(CHR_ENC_CODE_PAGES[charset], input); } else { - output[charset] = Utils.arrayBufferToStr(cptable.utils.encode(IO_FORMAT[charset], input)); + output[charset] = Utils.arrayBufferToStr(cptable.utils.encode(CHR_ENC_CODE_PAGES[charset], input)); } } catch (err) { output[charset] = "Could not decode."; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 820b1a8d..793b61de 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -180,7 +180,6 @@ class Manager { document.getElementById("save-all-to-file").addEventListener("click", this.output.saveAllClick.bind(this.output)); document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output)); document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output)); - document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output)); document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output)); document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output)); this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); diff --git a/src/web/html/index.html b/src/web/html/index.html index a7931de5..68d69a78 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -300,9 +300,6 @@ - diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index ea15b6ac..185b3bdb 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -224,7 +224,7 @@ #output-file { position: absolute; left: 0; - bottom: 0; + top: 50%; width: 100%; display: none; } @@ -446,6 +446,10 @@ /* Status bar */ +.cm-panel input::placeholder { + font-size: 12px !important; +} + .ͼ2 .cm-panels { background-color: var(--secondary-background-colour); border-color: var(--secondary-border-colour); @@ -509,12 +513,38 @@ background-color: #ddd } -/* Show the dropup menu on hover */ -.cm-status-bar-select:hover .cm-status-bar-select-content { - display: block; -} - /* Change the background color of the dropup button when the dropup content is shown */ .cm-status-bar-select:hover .cm-status-bar-select-btn { background-color: #f1f1f1; } + +/* The search field */ +.cm-status-bar-filter-input { + box-sizing: border-box; + font-size: 12px; + padding-left: 10px !important; + border: none; +} + +.cm-status-bar-filter-search { + border-top: 1px solid #ddd; +} + +/* Show the dropup menu */ +.cm-status-bar-select .show { + display: block; +} + +.cm-status-bar-select-scroll { + overflow-y: auto; + max-height: 300px; +} + +.chr-enc-value { + max-width: 150px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; +} \ No newline at end of file diff --git a/src/web/utils/htmlWidget.mjs b/src/web/utils/htmlWidget.mjs index 5e5c41c1..34800933 100644 --- a/src/web/utils/htmlWidget.mjs +++ b/src/web/utils/htmlWidget.mjs @@ -65,9 +65,11 @@ class HTMLWidget extends WidgetType { */ replaceControlChars(textNode) { const val = escapeControlChars(textNode.nodeValue, true, this.view.state.lineBreak); - const node = document.createElement("null"); - node.innerHTML = val; - textNode.parentNode.replaceChild(node, textNode); + if (val.length !== textNode.nodeValue.length) { + const node = document.createElement("span"); + node.innerHTML = val; + textNode.parentNode.replaceChild(node, textNode); + } } } @@ -119,8 +121,7 @@ export function htmlPlugin(htmlOutput) { } } }, { - decorations: v => v.decorations, - + decorations: v => v.decorations } ); diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs index 431d8a3d..f9be5006 100644 --- a/src/web/utils/statusBar.mjs +++ b/src/web/utils/statusBar.mjs @@ -5,6 +5,7 @@ */ import {showPanel} from "@codemirror/view"; +import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs"; /** * A Status bar extension for CodeMirror @@ -19,6 +20,10 @@ class StatusBarPanel { this.label = opts.label; this.bakeStats = opts.bakeStats ? opts.bakeStats : null; this.eolHandler = opts.eolHandler; + this.chrEncHandler = opts.chrEncHandler; + + this.eolVal = null; + this.chrEncVal = null; this.dom = this.buildDOM(); } @@ -40,19 +45,42 @@ class StatusBarPanel { dom.appendChild(rhs); // Event listeners - dom.addEventListener("click", this.eolSelectClick.bind(this), false); + dom.querySelectorAll(".cm-status-bar-select-btn").forEach( + el => el.addEventListener("click", this.showDropUp.bind(this), false) + ); + dom.querySelector(".eol-select").addEventListener("click", this.eolSelectClick.bind(this), false); + dom.querySelector(".chr-enc-select").addEventListener("click", this.chrEncSelectClick.bind(this), false); + dom.querySelector(".cm-status-bar-filter-input").addEventListener("keyup", this.chrEncFilter.bind(this), false); return dom; } + /** + * Handler for dropup clicks + * Shows/Hides the dropup + * @param {Event} e + */ + showDropUp(e) { + const el = e.target + .closest(".cm-status-bar-select") + .querySelector(".cm-status-bar-select-content"); + + el.classList.add("show"); + + // Focus the filter input if present + const filter = el.querySelector(".cm-status-bar-filter-input"); + if (filter) filter.focus(); + + // Set up a listener to close the menu if the user clicks outside of it + hideOnClickOutside(el, e); + } + /** * Handler for EOL Select clicks * Sets the line separator * @param {Event} e */ eolSelectClick(e) { - e.preventDefault(); - const eolLookup = { "LF": "\u000a", "VT": "\u000b", @@ -65,8 +93,46 @@ class StatusBarPanel { }; const eolval = eolLookup[e.target.getAttribute("data-val")]; + if (eolval === undefined) return; + // Call relevant EOL change handler this.eolHandler(eolval); + hideElement(e.target.closest(".cm-status-bar-select-content")); + } + + /** + * Handler for Chr Enc Select clicks + * Sets the character encoding + * @param {Event} e + */ + chrEncSelectClick(e) { + const chrEncVal = parseInt(e.target.getAttribute("data-val"), 10); + + if (isNaN(chrEncVal)) return; + + this.chrEncHandler(chrEncVal); + this.updateCharEnc(chrEncVal); + hideElement(e.target.closest(".cm-status-bar-select-content")); + } + + /** + * Handler for Chr Enc keyup events + * Filters the list of selectable character encodings + * @param {Event} e + */ + chrEncFilter(e) { + const input = e.target; + const filter = input.value.toLowerCase(); + const div = input.closest(".cm-status-bar-select-content"); + const a = div.getElementsByTagName("a"); + for (let i = 0; i < a.length; i++) { + const txtValue = a[i].textContent || a[i].innerText; + if (txtValue.toLowerCase().includes(filter)) { + a[i].style.display = "block"; + } else { + a[i].style.display = "none"; + } + } } /** @@ -121,33 +187,48 @@ class StatusBarPanel { } /** - * Gets the current character encoding of the document - * @param {EditorState} state - */ - updateCharEnc(state) { - // const charenc = this.dom.querySelector("#char-enc-value"); - // TODO - // charenc.textContent = "TODO"; - } - - /** - * Returns what the current EOL separator is set to + * Sets the current EOL separator in the status bar * @param {EditorState} state */ updateEOL(state) { + if (state.lineBreak === this.eolVal) return; + const eolLookup = { - "\u000a": "LF", - "\u000b": "VT", - "\u000c": "FF", - "\u000d": "CR", - "\u000d\u000a": "CRLF", - "\u0085": "NEL", - "\u2028": "LS", - "\u2029": "PS" + "\u000a": ["LF", "Line Feed"], + "\u000b": ["VT", "Vertical Tab"], + "\u000c": ["FF", "Form Feed"], + "\u000d": ["CR", "Carriage Return"], + "\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"], + "\u0085": ["NEL", "Next Line"], + "\u2028": ["LS", "Line Separator"], + "\u2029": ["PS", "Paragraph Separator"] }; const val = this.dom.querySelector(".eol-value"); - val.textContent = eolLookup[state.lineBreak]; + const button = val.closest(".cm-status-bar-select-btn"); + const eolName = eolLookup[state.lineBreak]; + val.textContent = eolName[0]; + button.setAttribute("title", `End of line sequence: ${eolName[1]}`); + button.setAttribute("data-original-title", `End of line sequence: ${eolName[1]}`); + this.eolVal = state.lineBreak; + } + + + /** + * Gets the current character encoding of the document + * @param {number} chrEncVal + */ + updateCharEnc(chrEncVal) { + if (chrEncVal === this.chrEncVal) return; + + const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes"; + + const val = this.dom.querySelector(".chr-enc-value"); + const button = val.closest(".cm-status-bar-select-btn"); + val.textContent = name; + button.setAttribute("title", `${this.label} character encoding: ${name}`); + button.setAttribute("data-original-title", `${this.label} character encoding: ${name}`); + this.chrEncVal = chrEncVal; } /** @@ -168,6 +249,19 @@ class StatusBarPanel { } } + /** + * Updates the sizing of elements that need to fit correctly + * @param {EditorView} view + */ + updateSizing(view) { + const viewHeight = view.contentDOM.clientHeight; + this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach( + el => { + el.style.maxHeight = (viewHeight - 50) + "px"; + } + ); + } + /** * Builds the Left-hand-side widgets * @returns {string} @@ -197,39 +291,98 @@ class StatusBarPanel { /** * Builds the Right-hand-side widgets * Event listener set up in Manager + * * @returns {string} */ constructRHS() { + const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name => + `${name}` + ).join(""); + return ` - - language - UTF-16 - +
    + + text_fields Raw Bytes + +
    +
    + Raw Bytes + ${chrEncOptions} +
    + +
    +
    keyboard_return - `; } } +const elementsWithListeners = {}; + +/** + * Hides the provided element when a click is made outside of it + * @param {Element} element + * @param {Event} instantiatingEvent + */ +function hideOnClickOutside(element, instantiatingEvent) { + /** + * Handler for document click events + * Closes element if click is outside it. + * @param {Event} event + */ + const outsideClickListener = event => { + // Don't trigger if we're clicking inside the element, or if the element + // is not visible, or if this is the same click event that opened it. + if (!element.contains(event.target) && + event.timeStamp !== instantiatingEvent.timeStamp) { + hideElement(element); + } + }; + + if (!Object.keys(elementsWithListeners).includes(element)) { + document.addEventListener("click", outsideClickListener); + elementsWithListeners[element] = outsideClickListener; + } +} + +/** + * Hides the specified element and removes the click listener for it + * @param {Element} element + */ +function hideElement(element) { + element.classList.remove("show"); + document.removeEventListener("click", elementsWithListeners[element]); + delete elementsWithListeners[element]; +} + + /** * A panel constructor factory building a panel that re-counts the stats every time the document changes. * @param {Object} opts @@ -240,7 +393,7 @@ function makePanel(opts) { return (view) => { sbPanel.updateEOL(view.state); - sbPanel.updateCharEnc(view.state); + sbPanel.updateCharEnc(opts.initialChrEncVal); sbPanel.updateBakeStats(); sbPanel.updateStats(view.state.doc); sbPanel.updateSelection(view.state, false); @@ -250,8 +403,10 @@ function makePanel(opts) { update(update) { sbPanel.updateEOL(update.state); sbPanel.updateSelection(update.state, update.selectionSet); - sbPanel.updateCharEnc(update.state); sbPanel.updateBakeStats(); + if (update.geometryChanged) { + sbPanel.updateSizing(update.view); + } if (update.docChanged) { sbPanel.updateStats(update.state.doc); } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index ed8f174b..caa1a098 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -10,6 +10,7 @@ import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker import Utils, {debounce} from "../../core/Utils.mjs"; import {toBase64} from "../../core/lib/Base64.mjs"; import {isImage} from "../../core/lib/FileType.mjs"; +import cptable from "codepage"; import { EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor, dropCursor @@ -39,6 +40,7 @@ class InputWaiter { this.manager = manager; this.inputTextEl = document.getElementById("input-text"); + this.inputChrEnc = 0; this.initEditor(); this.inputWorker = null; @@ -84,7 +86,9 @@ class InputWaiter { // Custom extensions statusBar({ label: "Input", - eolHandler: this.eolChange.bind(this) + eolHandler: this.eolChange.bind(this), + chrEncHandler: this.chrEncChange.bind(this), + initialChrEncVal: this.inputChrEnc }), // Mutable state @@ -122,19 +126,30 @@ class InputWaiter { /** * Handler for EOL change events * Sets the line separator + * @param {string} eolVal */ - eolChange(eolval) { + eolChange(eolVal) { const oldInputVal = this.getInput(); // Update the EOL value this.inputEditorView.dispatch({ - effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval)) + effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal)) }); // Reset the input so that lines are recalculated, preserving the old EOL values this.setInput(oldInputVal); } + /** + * Handler for Chr Enc change events + * Sets the input character encoding + * @param {number} chrEncVal + */ + chrEncChange(chrEncVal) { + this.inputChrEnc = chrEncVal; + this.inputChange(); + } + /** * Sets word wrap on the input editor * @param {boolean} wrap @@ -380,7 +395,7 @@ class InputWaiter { this.showLoadingInfo(r.data, true); break; case "setInput": - this.set(r.data.inputObj, r.data.silent); + this.set(r.data.inputNum, r.data.inputObj, r.data.silent); break; case "inputAdded": this.inputAdded(r.data.changeTab, r.data.inputNum); @@ -403,9 +418,6 @@ class InputWaiter { case "setUrl": this.setUrl(r.data); break; - case "inputSwitch": - this.manager.output.inputSwitch(r.data); - break; case "getInput": case "getInputNums": this.callbacks[r.data.id](r.data); @@ -435,22 +447,36 @@ class InputWaiter { /** * Sets the input in the input area * - * @param {object} inputData - Object containing the input and its metadata - * @param {number} inputData.inputNum - The unique inputNum for the selected input - * @param {string | object} inputData.input - The actual input data - * @param {string} inputData.name - The name of the input file - * @param {number} inputData.size - The size in bytes of the input file - * @param {string} inputData.type - The MIME type of the input file - * @param {number} inputData.progress - The load progress of the input file + * @param {number} inputNum + * @param {Object} inputData - Object containing the input and its metadata + * @param {string} type + * @param {ArrayBuffer} buffer + * @param {string} stringSample + * @param {Object} file + * @param {string} file.name + * @param {number} file.size + * @param {string} file.type + * @param {string} status + * @param {number} progress * @param {boolean} [silent=false] - If false, fires the manager statechange event */ - async set(inputData, silent=false) { + async set(inputNum, inputData, silent=false) { return new Promise(function(resolve, reject) { const activeTab = this.manager.tabs.getActiveInputTab(); - if (inputData.inputNum !== activeTab) return; + if (inputNum !== activeTab) return; - if (typeof inputData.input === "string") { - this.setInput(inputData.input); + if (inputData.file) { + this.setFile(inputNum, inputData, silent); + } else { + // TODO Per-tab encodings? + let inputVal; + if (this.inputChrEnc > 0) { + inputVal = cptable.utils.decode(this.inputChrEnc, new Uint8Array(inputData.buffer)); + } else { + inputVal = Utils.arrayBufferToStr(inputData.buffer); + } + + this.setInput(inputVal); const fileOverlay = document.getElementById("input-file"), fileName = document.getElementById("input-file-name"), fileSize = document.getElementById("input-file-size"), @@ -466,8 +492,8 @@ class InputWaiter { this.inputTextEl.classList.remove("blur"); // Set URL to current input - const inputStr = toBase64(inputData.input, "A-Za-z0-9+/"); - if (inputStr.length >= 0 && inputStr.length <= 68267) { + if (inputVal.length >= 0 && inputVal.length <= 51200) { + const inputStr = toBase64(inputVal, "A-Za-z0-9+/"); this.setUrl({ includeInput: true, input: inputStr @@ -475,8 +501,6 @@ class InputWaiter { } if (!silent) window.dispatchEvent(this.manager.statechange); - } else { - this.setFile(inputData, silent); } }.bind(this)); @@ -485,18 +509,22 @@ class InputWaiter { /** * Displays file details * - * @param {object} inputData - Object containing the input and its metadata - * @param {number} inputData.inputNum - The unique inputNum for the selected input - * @param {string | object} inputData.input - The actual input data - * @param {string} inputData.name - The name of the input file - * @param {number} inputData.size - The size in bytes of the input file - * @param {string} inputData.type - The MIME type of the input file - * @param {number} inputData.progress - The load progress of the input file + * @param {number} inputNum + * @param {Object} inputData - Object containing the input and its metadata + * @param {string} type + * @param {ArrayBuffer} buffer + * @param {string} stringSample + * @param {Object} file + * @param {string} file.name + * @param {number} file.size + * @param {string} file.type + * @param {string} status + * @param {number} progress * @param {boolean} [silent=true] - If false, fires the manager statechange event */ - setFile(inputData, silent=true) { + setFile(inputNum, inputData, silent=true) { const activeTab = this.manager.tabs.getActiveInputTab(); - if (inputData.inputNum !== activeTab) return; + if (inputNum !== activeTab) return; const fileOverlay = document.getElementById("input-file"), fileName = document.getElementById("input-file-name"), @@ -505,9 +533,9 @@ class InputWaiter { fileLoaded = document.getElementById("input-file-loaded"); fileOverlay.style.display = "block"; - fileName.textContent = inputData.name; - fileSize.textContent = inputData.size + " bytes"; - fileType.textContent = inputData.type; + fileName.textContent = inputData.file.name; + fileSize.textContent = inputData.file.size + " bytes"; + fileType.textContent = inputData.file.type; if (inputData.status === "error") { fileLoaded.textContent = "Error"; fileLoaded.style.color = "#FF0000"; @@ -516,7 +544,7 @@ class InputWaiter { fileLoaded.textContent = inputData.progress + "%"; } - this.displayFilePreview(inputData); + this.displayFilePreview(inputNum, inputData); if (!silent) window.dispatchEvent(this.manager.statechange); } @@ -583,19 +611,18 @@ class InputWaiter { /** * Shows a chunk of the file in the input behind the file overlay * + * @param {number} inputNum - The inputNum of the file being displayed * @param {Object} inputData - Object containing the input data - * @param {number} inputData.inputNum - The inputNum of the file being displayed - * @param {ArrayBuffer} inputData.input - The actual input to display + * @param {string} inputData.stringSample - The first 4096 bytes of input as a string */ - displayFilePreview(inputData) { + displayFilePreview(inputNum, inputData) { const activeTab = this.manager.tabs.getActiveInputTab(), - input = inputData.input; - if (inputData.inputNum !== activeTab) return; + input = inputData.buffer; + if (inputNum !== activeTab) return; this.inputTextEl.classList.add("blur"); - this.setInput(Utils.arrayBufferToStr(input.slice(0, 4096))); + this.setInput(input.stringSample); this.renderFileThumb(); - } /** @@ -623,46 +650,40 @@ class InputWaiter { * * @param {number} inputNum * @param {string | ArrayBuffer} value - * @param {boolean} [force=false] - If true, forces the value to be updated even if the type is different to the currently stored type */ updateInputValue(inputNum, value, force=false) { - let includeInput = false; - const recipeStr = toBase64(value, "A-Za-z0-9+/"); // B64 alphabet with no padding - if (recipeStr.length > 0 && recipeStr.length <= 68267) { - includeInput = true; + // Prepare the value as a buffer (full value) and a string sample (up to 4096 bytes) + let buffer; + let stringSample = ""; + + // If value is a string, interpret it using the specified character encoding + if (typeof value === "string") { + stringSample = value.slice(0, 4096); + if (this.inputChrEnc > 0) { + buffer = cptable.utils.encode(this.inputChrEnc, value); + buffer = new Uint8Array(buffer).buffer; + } else { + buffer = Utils.strToArrayBuffer(value); + } + } else { + buffer = value; + stringSample = Utils.arrayBufferToStr(value.slice(0, 4096)); } + + + const recipeStr = buffer.byteLength < 51200 ? toBase64(buffer, "A-Za-z0-9+/") : ""; // B64 alphabet with no padding this.setUrl({ - includeInput: includeInput, + includeInput: recipeStr.length > 0 && buffer.byteLength < 51200, input: recipeStr }); - // Value is either a string set by the input or an ArrayBuffer from a LoaderWorker, - // so is safe to use typeof === "string" - const transferable = (typeof value !== "string") ? [value] : undefined; + const transferable = [buffer]; this.inputWorker.postMessage({ action: "updateInputValue", data: { inputNum: inputNum, - value: value, - force: force - } - }, transferable); - } - - /** - * Updates the .data property for the input of the specified inputNum. - * Used for switching the output into the input - * - * @param {number} inputNum - The inputNum of the input we're changing - * @param {object} inputData - The new data object - */ - updateInputObj(inputNum, inputData) { - const transferable = (typeof inputData !== "string") ? [inputData.fileBuffer] : undefined; - this.inputWorker.postMessage({ - action: "updateInputObj", - data: { - inputNum: inputNum, - data: inputData + buffer: buffer, + stringSample: stringSample } }, transferable); } @@ -1052,9 +1073,8 @@ class InputWaiter { this.updateInputValue(inputNum, "", true); - this.set({ - inputNum: inputNum, - input: "" + this.set(inputNum, { + buffer: new ArrayBuffer() }); this.manager.tabs.updateInputTabHeader(inputNum, ""); diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index deaeaed3..f0b03d72 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -9,6 +9,7 @@ import Utils, {debounce} from "../../core/Utils.mjs"; import Dish from "../../core/Dish.mjs"; import FileSaver from "file-saver"; import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs"; +import cptable from "codepage"; import { EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor @@ -48,6 +49,7 @@ class OutputWaiter { html: "", changed: false }; + this.outputChrEnc = 0; this.initEditor(); this.outputs = {}; @@ -86,7 +88,9 @@ class OutputWaiter { statusBar({ label: "Output", bakeStats: this.bakeStats, - eolHandler: this.eolChange.bind(this) + eolHandler: this.eolChange.bind(this), + chrEncHandler: this.chrEncChange.bind(this), + initialChrEncVal: this.outputChrEnc }), htmlPlugin(this.htmlOutput), copyOverride(), @@ -119,19 +123,29 @@ class OutputWaiter { /** * Handler for EOL change events * Sets the line separator + * @param {string} eolVal */ - eolChange(eolval) { + eolChange(eolVal) { const oldOutputVal = this.getOutput(); // Update the EOL value this.outputEditorView.dispatch({ - effects: this.outputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval)) + effects: this.outputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal)) }); // Reset the output so that lines are recalculated, preserving the old EOL values this.setOutput(oldOutputVal); } + /** + * Handler for Chr Enc change events + * Sets the output character encoding + * @param {number} chrEncVal + */ + chrEncChange(chrEncVal) { + this.outputChrEnc = chrEncVal; + } + /** * Sets word wrap on the output editor * @param {boolean} wrap @@ -193,7 +207,8 @@ class OutputWaiter { }); // Execute script sections - const scriptElements = document.getElementById("output-html").querySelectorAll("script"); + const outputHTML = document.getElementById("output-html"); + const scriptElements = outputHTML ? outputHTML.querySelectorAll("script") : []; for (let i = 0; i < scriptElements.length; i++) { try { eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval @@ -405,8 +420,6 @@ class OutputWaiter { removeAllOutputs() { this.outputs = {}; - this.resetSwitch(); - const tabsList = document.getElementById("output-tabs"); const tabsListChildren = tabsList.children; @@ -418,19 +431,18 @@ class OutputWaiter { } /** - * Sets the output in the output textarea. + * Sets the output in the output pane. * * @param {number} inputNum */ async set(inputNum) { + inputNum = parseInt(inputNum, 10); if (inputNum !== this.manager.tabs.getActiveOutputTab() || !this.outputExists(inputNum)) return; this.toggleLoader(true); return new Promise(async function(resolve, reject) { - const output = this.outputs[inputNum], - activeTab = this.manager.tabs.getActiveOutputTab(); - if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10); + const output = this.outputs[inputNum]; const outputFile = document.getElementById("output-file"); @@ -491,17 +503,33 @@ class OutputWaiter { switch (output.data.type) { case "html": outputFile.style.display = "none"; + // TODO what if the HTML content needs to be in a certain character encoding? + // Grey out chr enc selection? Set back to Raw Bytes? this.setHTMLOutput(output.data.result); break; - case "ArrayBuffer": + case "ArrayBuffer": { this.outputTextEl.style.display = "block"; + outputFile.style.display = "none"; this.clearHTMLOutput(); - this.setOutput(""); - this.setFile(await this.getDishBuffer(output.data.dish), activeTab); + let outputVal = ""; + if (this.outputChrEnc === 0) { + outputVal = Utils.arrayBufferToStr(output.data.result); + } else { + try { + outputVal = cptable.utils.decode(this.outputChrEnc, new Uint8Array(output.data.result)); + } catch (err) { + outputVal = err; + } + } + + this.setOutput(outputVal); + + // this.setFile(await this.getDishBuffer(output.data.dish), activeTab); break; + } case "string": default: this.outputTextEl.style.display = "block"; @@ -1333,7 +1361,6 @@ class OutputWaiter { */ async switchClick() { const activeTab = this.manager.tabs.getActiveOutputTab(); - const transferable = []; const switchButton = document.getElementById("switch"); switchButton.classList.add("spin"); @@ -1341,82 +1368,15 @@ class OutputWaiter { switchButton.firstElementChild.innerHTML = "autorenew"; $(switchButton).tooltip("hide"); - let active = await this.getDishBuffer(this.getOutputDish(activeTab)); + const activeData = await this.getDishBuffer(this.getOutputDish(activeTab)); - if (!this.outputExists(activeTab)) { - this.resetSwitchButton(); - return; - } - - if (this.outputs[activeTab].data.type === "string" && - active.byteLength <= this.app.options.ioDisplayThreshold * 1024) { - const dishString = await this.getDishStr(this.getOutputDish(activeTab)); - active = dishString; - } else { - transferable.push(active); - } - - this.manager.input.inputWorker.postMessage({ - action: "inputSwitch", - data: { + if (this.outputExists(activeTab)) { + this.manager.input.set({ inputNum: activeTab, - outputData: active - } - }, transferable); - } - - /** - * Handler for when the inputWorker has switched the inputs. - * Stores the old input - * - * @param {object} switchData - * @param {number} switchData.inputNum - * @param {string | object} switchData.data - * @param {ArrayBuffer} switchData.data.fileBuffer - * @param {number} switchData.data.size - * @param {string} switchData.data.type - * @param {string} switchData.data.name - */ - inputSwitch(switchData) { - this.switchOrigData = switchData; - document.getElementById("undo-switch").disabled = false; - - this.resetSwitchButton(); - - } - - /** - * Handler for undo switch click events. - * Removes the output from the input and replaces the input that was removed. - */ - undoSwitchClick() { - this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data); - - this.manager.input.fileLoaded(this.switchOrigData.inputNum); - - this.resetSwitch(); - } - - /** - * Removes the switch data and resets the switch buttons - */ - resetSwitch() { - if (this.switchOrigData !== undefined) { - delete this.switchOrigData; + input: activeData + }); } - const undoSwitch = document.getElementById("undo-switch"); - undoSwitch.disabled = true; - $(undoSwitch).tooltip("hide"); - - this.resetSwitchButton(); - } - - /** - * Resets the switch button to its usual state - */ - resetSwitchButton() { - const switchButton = document.getElementById("switch"); switchButton.classList.remove("spin"); switchButton.disabled = false; switchButton.firstElementChild.innerHTML = "open_in_browser"; diff --git a/src/web/workers/InputWorker.mjs b/src/web/workers/InputWorker.mjs index 9912995b..e1c75de9 100644 --- a/src/web/workers/InputWorker.mjs +++ b/src/web/workers/InputWorker.mjs @@ -3,12 +3,12 @@ * Handles storage, modification and retrieval of the inputs. * * @author j433866 [j433866@gmail.com] + * @author n1474335 [n1474335@gmail.com] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ import Utils from "../../core/Utils.mjs"; -import {detectFileType} from "../../core/lib/FileType.mjs"; // Default max values // These will be correctly calculated automatically @@ -16,6 +16,21 @@ self.maxWorkers = 4; self.maxTabs = 1; self.pendingFiles = []; + +/** + * Dictionary of inputs keyed on the inputNum + * Each entry is an object with the following type: + * @typedef {Object} Input + * @property {string} type + * @property {ArrayBuffer} buffer + * @property {string} stringSample + * @property {Object} file + * @property {string} file.name + * @property {number} file.size + * @property {string} file.type + * @property {string} status + * @property {number} progress + */ self.inputs = {}; self.loaderWorkers = []; self.currentInputNum = 1; @@ -53,9 +68,6 @@ self.addEventListener("message", function(e) { case "updateInputValue": self.updateInputValue(r.data); break; - case "updateInputObj": - self.updateInputObj(r.data); - break; case "updateInputProgress": self.updateInputProgress(r.data); break; @@ -75,7 +87,7 @@ self.addEventListener("message", function(e) { log.setLevel(r.data, false); break; case "addInput": - self.addInput(r.data, "string"); + self.addInput(r.data, "userinput"); break; case "refreshTabs": self.refreshTabs(r.data.inputNum, r.data.direction); @@ -98,9 +110,6 @@ self.addEventListener("message", function(e) { case "loaderWorkerMessage": self.handleLoaderMessage(r.data); break; - case "inputSwitch": - self.inputSwitch(r.data); - break; case "updateTabHeader": self.updateTabHeader(r.data); break; @@ -213,13 +222,10 @@ self.bakeInput = function(inputNum, bakeId) { return; } - let inputData = inputObj.data; - if (typeof inputData !== "string") inputData = inputData.fileBuffer; - self.postMessage({ action: "queueInput", data: { - input: inputData, + input: inputObj.buffer, inputNum: inputNum, bakeId: bakeId } @@ -236,23 +242,6 @@ self.getInputObj = function(inputNum) { return self.inputs[inputNum]; }; -/** - * Gets the stored value for a specific inputNum. - * - * @param {number} inputNum - The input we want to get the value of - * @returns {string | ArrayBuffer} - */ -self.getInputValue = function(inputNum) { - if (self.inputs[inputNum]) { - if (typeof self.inputs[inputNum].data === "string") { - return self.inputs[inputNum].data; - } else { - return self.inputs[inputNum].data.fileBuffer; - } - } - return ""; -}; - /** * Gets the stored value or object for a specific inputNum and sends it to the inputWaiter. * @@ -263,7 +252,7 @@ self.getInputValue = function(inputNum) { */ self.getInput = function(inputData) { const inputNum = inputData.inputNum, - data = (inputData.getObj) ? self.getInputObj(inputNum) : self.getInputValue(inputNum); + data = (inputData.getObj) ? self.getInputObj(inputNum) : self.inputs[inputNum].buffer; self.postMessage({ action: "getInput", data: { @@ -421,17 +410,15 @@ self.getNearbyNums = function(inputNum, direction) { self.updateTabHeader = function(inputNum) { const input = self.getInputObj(inputNum); if (input === null || input === undefined) return; - let inputData = input.data; - if (typeof inputData !== "string") { - inputData = input.data.name; - } - inputData = inputData.replace(/[\n\r]/g, ""); + + let header = input.type === "file" ? input.file.name : input.stringSample; + header = header.slice(0, 100).replace(/[\n\r]/g, ""); self.postMessage({ action: "updateTabHeader", data: { inputNum: inputNum, - input: inputData.slice(0, 100) + input: header } }); }; @@ -450,37 +437,15 @@ self.setInput = function(inputData) { const input = self.getInputObj(inputNum); if (input === undefined || input === null) return; - let inputVal = input.data; - const inputObj = { - inputNum: inputNum, - input: inputVal - }; - if (typeof inputVal !== "string") { - inputObj.name = inputVal.name; - inputObj.size = inputVal.size; - inputObj.type = inputVal.type; - inputObj.progress = input.progress; - inputObj.status = input.status; - inputVal = inputVal.fileBuffer; - const fileSlice = inputVal.slice(0, 512001); - inputObj.input = fileSlice; + self.postMessage({ + action: "setInput", + data: { + inputNum: inputNum, + inputObj: input, + silent: silent + } + }); - self.postMessage({ - action: "setInput", - data: { - inputObj: inputObj, - silent: silent - } - }, [fileSlice]); - } else { - self.postMessage({ - action: "setInput", - data: { - inputObj: inputObj, - silent: silent - } - }); - } self.updateTabHeader(inputNum); }; @@ -546,54 +511,23 @@ self.updateInputProgress = function(inputData) { * * @param {object} inputData * @param {number} inputData.inputNum - The input that's having its value updated - * @param {string | ArrayBuffer} inputData.value - The new value of the input - * @param {boolean} inputData.force - If true, still updates the input value if the input type is different to the stored value + * @param {ArrayBuffer} inputData.buffer - The new value of the input as a buffer + * @param {string} [inputData.stringSample] - A sample of the value as a string (truncated to 4096 chars) */ self.updateInputValue = function(inputData) { - const inputNum = inputData.inputNum; + const inputNum = parseInt(inputData.inputNum, 10); if (inputNum < 1) return; - if (Object.prototype.hasOwnProperty.call(self.inputs[inputNum].data, "fileBuffer") && - typeof inputData.value === "string" && !inputData.force) return; - const value = inputData.value; - if (self.inputs[inputNum] !== undefined) { - if (typeof value === "string") { - self.inputs[inputNum].data = value; - } else { - self.inputs[inputNum].data.fileBuffer = value; - } - self.inputs[inputNum].status = "loaded"; - self.inputs[inputNum].progress = 100; - return; + + if (!Object.prototype.hasOwnProperty.call(self.inputs, inputNum)) + throw new Error(`No input with ID ${inputNum} exists`); + + self.inputs[inputNum].buffer = inputData.buffer; + if (!("stringSample" in inputData)) { + inputData.stringSample = Utils.arrayBufferToStr(inputData.buffer.slice(0, 4096)); } - - // If we get to here, an input for inputNum could not be found, - // so create a new one. Only do this if the value is a string, as - // loadFiles will create the input object for files - if (typeof value === "string") { - self.inputs.push({ - inputNum: inputNum, - data: value, - status: "loaded", - progress: 100 - }); - } -}; - -/** - * Update the stored data object for an input. - * Used if we need to change a string to an ArrayBuffer - * - * @param {object} inputData - * @param {number} inputData.inputNum - The number of the input we're updating - * @param {object} inputData.data - The new data object for the input - */ -self.updateInputObj = function(inputData) { - const inputNum = inputData.inputNum; - const data = inputData.data; - - if (self.getInputObj(inputNum) === undefined) return; - - self.inputs[inputNum].data = data; + self.inputs[inputNum].stringSample = inputData.stringSample; + self.inputs[inputNum].status = "loaded"; + self.inputs[inputNum].progress = 100; }; /** @@ -632,8 +566,7 @@ self.loaderWorkerReady = function(workerData) { /** * Handler for messages sent by loaderWorkers. - * (Messages are sent between the inputWorker and - * loaderWorkers via the main thread) + * (Messages are sent between the inputWorker and loaderWorkers via the main thread) * * @param {object} r - The data sent by the loaderWorker * @param {number} r.inputNum - The inputNum which the message corresponds to @@ -667,7 +600,7 @@ self.handleLoaderMessage = function(r) { self.updateInputValue({ inputNum: inputNum, - value: r.fileBuffer + buffer: r.fileBuffer }); self.postMessage({ @@ -757,7 +690,8 @@ self.loadFiles = function(filesData) { let lastInputNum = -1; const inputNums = []; for (let i = 0; i < files.length; i++) { - if (i === 0 && self.getInputValue(activeTab) === "") { + // If the first input is empty, replace it rather than adding a new one + if (i === 0 && (!self.inputs[activeTab].buffer || self.inputs[activeTab].buffer.byteLength === 0)) { self.removeInput({ inputNum: activeTab, refreshTabs: false, @@ -798,7 +732,7 @@ self.loadFiles = function(filesData) { * Adds an input to the input dictionary * * @param {boolean} [changetab=false] - Whether or not to change to the new input - * @param {string} type - Either "string" or "file" + * @param {string} type - Either "userinput" or "file" * @param {Object} fileData - Contains information about the file to be added to the input (only used when type is "file") * @param {string} fileData.name - The filename of the input being added * @param {number} fileData.size - The file size (in bytes) of the input being added @@ -810,25 +744,30 @@ self.addInput = function( type, fileData = { name: "unknown", - size: "unknown", + size: 0, type: "unknown" }, inputNum = self.currentInputNum++ ) { self.numInputs++; const newInputObj = { - inputNum: inputNum + type: null, + buffer: new ArrayBuffer(), + stringSample: "", + file: null, + status: "pending", + progress: 0 }; switch (type) { - case "string": - newInputObj.data = ""; + case "userinput": + newInputObj.type = "userinput"; newInputObj.status = "loaded"; newInputObj.progress = 100; break; case "file": - newInputObj.data = { - fileBuffer: new ArrayBuffer(), + newInputObj.type = "file"; + newInputObj.file = { name: fileData.name, size: fileData.size, type: fileData.type @@ -837,7 +776,7 @@ self.addInput = function( newInputObj.progress = 0; break; default: - log.error(`Invalid type '${type}'.`); + log.error(`Invalid input type '${type}'.`); return -1; } self.inputs[inputNum] = newInputObj; @@ -976,18 +915,18 @@ self.filterTabs = function(searchData) { self.inputs[iNum].status === "loading" && showLoading || self.inputs[iNum].status === "loaded" && showLoaded) { try { - if (typeof self.inputs[iNum].data === "string") { + if (self.inputs[iNum].type === "userinput") { if (filterType.toLowerCase() === "content" && - filterExp.test(self.inputs[iNum].data.slice(0, 4096))) { - textDisplay = self.inputs[iNum].data.slice(0, 4096); + filterExp.test(self.inputs[iNum].stringSample)) { + textDisplay = self.inputs[iNum].stringSample; addInput = true; } } else { if ((filterType.toLowerCase() === "filename" && - filterExp.test(self.inputs[iNum].data.name)) || - filterType.toLowerCase() === "content" && - filterExp.test(Utils.arrayBufferToStr(self.inputs[iNum].data.fileBuffer.slice(0, 4096)))) { - textDisplay = self.inputs[iNum].data.name; + filterExp.test(self.inputs[iNum].file.name)) || + (filterType.toLowerCase() === "content" && + filterExp.test(self.inputs[iNum].stringSample))) { + textDisplay = self.inputs[iNum].file.name; addInput = true; } } @@ -1021,61 +960,3 @@ self.filterTabs = function(searchData) { data: inputs }); }; - -/** - * Swaps the input and outputs, and sends the old input back to the main thread. - * - * @param {object} switchData - * @param {number} switchData.inputNum - The inputNum of the input to be switched to - * @param {string | ArrayBuffer} switchData.outputData - The data to switch to - */ -self.inputSwitch = function(switchData) { - const currentInput = self.getInputObj(switchData.inputNum); - const currentData = currentInput.data; - if (currentInput === undefined || currentInput === null) return; - - if (typeof switchData.outputData !== "string") { - const output = new Uint8Array(switchData.outputData), - types = detectFileType(output); - let type = "unknown", - ext = "dat"; - if (types.length) { - type = types[0].mime; - ext = types[0].extension.split(",", 1)[0]; - } - - // ArrayBuffer - self.updateInputObj({ - inputNum: switchData.inputNum, - data: { - fileBuffer: switchData.outputData, - name: `output.${ext}`, - size: switchData.outputData.byteLength.toLocaleString(), - type: type - } - }); - } else { - // String - self.updateInputValue({ - inputNum: switchData.inputNum, - value: switchData.outputData, - force: true - }); - } - - self.postMessage({ - action: "inputSwitch", - data: { - data: currentData, - inputNum: switchData.inputNum - } - }); - - self.postMessage({ - action: "fileLoaded", - data: { - inputNum: switchData.inputNum - } - }); - -}; From 16b79e32f6ea4e4a00984f2d5d8a854f8d4275a4 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 2 Sep 2022 14:33:41 +0100 Subject: [PATCH 206/686] File details are now displayed in a side panel and the input is still editable --- src/web/Manager.mjs | 2 - src/web/html/index.html | 15 -- src/web/stylesheets/layout/_io.css | 48 +++++- src/web/utils/fileDetails.mjs | 134 +++++++++++++++ src/web/utils/sidePanel.mjs | 254 +++++++++++++++++++++++++++++ src/web/waiters/InputWaiter.mjs | 213 ++++++++---------------- 6 files changed, 500 insertions(+), 166 deletions(-) create mode 100644 src/web/utils/fileDetails.mjs create mode 100644 src/web/utils/sidePanel.mjs diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 793b61de..730d6e2e 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -152,7 +152,6 @@ class Manager { this.addListeners("#input-wrapper", "dragover", this.input.inputDragover, this.input); this.addListeners("#input-wrapper", "dragleave", this.input.inputDragleave, this.input); this.addListeners("#input-wrapper", "drop", this.input.inputDrop, this.input); - document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input)); document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input)); document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input)); document.getElementById("btn-next-input-tab").addEventListener("mousedown", this.input.nextTabClick.bind(this.input)); @@ -218,7 +217,6 @@ class Manager { this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options); document.getElementById("theme").addEventListener("change", this.options.themeChange.bind(this.options)); document.getElementById("logLevel").addEventListener("change", this.options.logLevelChange.bind(this.options)); - document.getElementById("imagePreview").addEventListener("change", this.input.renderFileThumb.bind(this.input)); // Misc window.addEventListener("keydown", this.bindings.parseInput.bind(this.bindings)); diff --git a/src/web/html/index.html b/src/web/html/index.html index 68d69a78..6e2c60a3 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -265,21 +265,6 @@
    -
    -
    -
    -
    - -
    - - Name:
    - Size:
    - Type:
    - Loaded: -
    -
    -
    -
    diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 185b3bdb..9c64fe85 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -220,7 +220,6 @@ transition: all 0.5s ease; } -#input-file, #output-file { position: absolute; left: 0; @@ -450,9 +449,10 @@ font-size: 12px !important; } -.ͼ2 .cm-panels { +.ͼ2 .cm-panels, +.ͼ2 .cm-side-panels { background-color: var(--secondary-background-colour); - border-color: var(--secondary-border-colour); + border-color: var(--primary-border-colour); color: var(--primary-font-colour); } @@ -547,4 +547,44 @@ text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; -} \ No newline at end of file +} + + +/* File details panel */ + +.cm-file-details { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: auto; + padding-bottom: 21px; + height: 100%; +} + +.file-details-heading { + font-weight: bold; + margin: 10px 0 10px 0; +} + +.file-details-data { + text-align: left; + margin: 10px 2px; +} + +.file-details-data td { + padding: 0 3px; + max-width: 130px; + min-width: 60px; + overflow: hidden; + vertical-align: top; + word-break: break-all; +} + +.file-details-error { + color: #f00; +} + +.file-details-thumbnail { + max-width: 180px; +} diff --git a/src/web/utils/fileDetails.mjs b/src/web/utils/fileDetails.mjs new file mode 100644 index 00000000..f8e3003b --- /dev/null +++ b/src/web/utils/fileDetails.mjs @@ -0,0 +1,134 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import {showSidePanel} from "./sidePanel.mjs"; +import Utils from "../../core/Utils.mjs"; +import {isImage} from "../../core/lib/FileType.mjs"; + +/** + * A File Details extension for CodeMirror + */ +class FileDetailsPanel { + + /** + * FileDetailsPanel constructor + * @param {Object} opts + */ + constructor(opts) { + this.fileDetails = opts.fileDetails; + this.progress = opts.progress; + this.status = opts.status; + this.buffer = opts.buffer; + this.renderPreview = opts.renderPreview; + this.dom = this.buildDOM(); + this.renderFileThumb(); + } + + /** + * Builds the file details DOM tree + * @returns {DOMNode} + */ + buildDOM() { + const dom = document.createElement("div"); + + dom.className = "cm-file-details"; + const fileThumb = require("../static/images/file-128x128.png"); + dom.innerHTML = ` +

    File details

    + +
    EncodingValue
    ${enc}${value}
    + + + + + + + + + + + + + + + + +
    Name: + ${Utils.escapeHtml(this.fileDetails.name)} +
    Size: + ${Utils.escapeHtml(this.fileDetails.size)} bytes +
    Type: + ${Utils.escapeHtml(this.fileDetails.type)} +
    Loaded: + ${this.status === "error" ? "Error" : this.progress + "%"} +
    + `; + + return dom; + } + + /** + * Render the file thumbnail + */ + renderFileThumb() { + if (!this.renderPreview) { + this.resetFileThumb(); + return; + } + const fileThumb = this.dom.querySelector(".file-details-thumbnail"); + const fileType = this.dom.querySelector(".file-details-type"); + const fileBuffer = new Uint8Array(this.buffer); + const type = isImage(fileBuffer); + + if (type && type !== "image/tiff" && fileBuffer.byteLength <= 512000) { + // Most browsers don't support displaying TIFFs, so ignore them + // Don't render images over 512,000 bytes + const blob = new Blob([fileBuffer], {type: type}), + url = URL.createObjectURL(blob); + fileThumb.src = url; + } else { + this.resetFileThumb(); + } + fileType.textContent = type; + } + + /** + * Reset the file thumbnail to the default icon + */ + resetFileThumb() { + const fileThumb = this.dom.querySelector(".file-details-thumbnail"); + fileThumb.src = require("../static/images/file-128x128.png"); + } + +} + +/** + * A panel constructor factory building a panel that displays file details + * @param {Object} opts + * @returns {Function} + */ +function makePanel(opts) { + const fdPanel = new FileDetailsPanel(opts); + + return (view) => { + return { + dom: fdPanel.dom, + width: 200, + update(update) { + } + }; + }; +} + +/** + * A function that build the extension that enables the panel in an editor. + * @param {Object} opts + * @returns {Extension} + */ +export function fileDetailsPanel(opts) { + const panelMaker = makePanel(opts); + return showSidePanel.of(panelMaker); +} diff --git a/src/web/utils/sidePanel.mjs b/src/web/utils/sidePanel.mjs new file mode 100644 index 00000000..a8de0931 --- /dev/null +++ b/src/web/utils/sidePanel.mjs @@ -0,0 +1,254 @@ +/** + * A modification of the CodeMirror Panel extension to enable panels to the + * left and right of the editor. + * Based on code here: https://github.com/codemirror/view/blob/main/src/panel.ts + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import {EditorView, ViewPlugin} from "@codemirror/view"; +import {Facet} from "@codemirror/state"; + +const panelConfig = Facet.define({ + combine(configs) { + let leftContainer, rightContainer; + for (const c of configs) { + leftContainer = leftContainer || c.leftContainer; + rightContainer = rightContainer || c.rightContainer; + } + return {leftContainer, rightContainer}; + } +}); + +/** + * Configures the panel-managing extension. + * @param {PanelConfig} config + * @returns Extension + */ +export function panels(config) { + return config ? [panelConfig.of(config)] : []; +} + +/** + * Get the active panel created by the given constructor, if any. + * This can be useful when you need access to your panels' DOM + * structure. + * @param {EditorView} view + * @param {PanelConstructor} panel + * @returns {Panel} + */ +export function getPanel(view, panel) { + const plugin = view.plugin(panelPlugin); + const index = plugin ? plugin.specs.indexOf(panel) : -1; + return index > -1 ? plugin.panels[index] : null; +} + +const panelPlugin = ViewPlugin.fromClass(class { + + /** + * @param {EditorView} view + */ + constructor(view) { + this.input = view.state.facet(showSidePanel); + this.specs = this.input.filter(s => s); + this.panels = this.specs.map(spec => spec(view)); + const conf = view.state.facet(panelConfig); + this.left = new PanelGroup(view, true, conf.leftContainer); + this.right = new PanelGroup(view, false, conf.rightContainer); + this.left.sync(this.panels.filter(p => p.left)); + this.right.sync(this.panels.filter(p => !p.left)); + for (const p of this.panels) { + p.dom.classList.add("cm-panel"); + if (p.mount) p.mount(); + } + } + + /** + * @param {ViewUpdate} update + */ + update(update) { + const conf = update.state.facet(panelConfig); + if (this.left.container !== conf.leftContainer) { + this.left.sync([]); + this.left = new PanelGroup(update.view, true, conf.leftContainer); + } + if (this.right.container !== conf.rightContainer) { + this.right.sync([]); + this.right = new PanelGroup(update.view, false, conf.rightContainer); + } + this.left.syncClasses(); + this.right.syncClasses(); + const input = update.state.facet(showSidePanel); + if (input !== this.input) { + const specs = input.filter(x => x); + const panels = [], left = [], right = [], mount = []; + for (const spec of specs) { + const known = this.specs.indexOf(spec); + let panel; + if (known < 0) { + panel = spec(update.view); + mount.push(panel); + } else { + panel = this.panels[known]; + if (panel.update) panel.update(update); + } + panels.push(panel) + ;(panel.left ? left : right).push(panel); + } + this.specs = specs; + this.panels = panels; + this.left.sync(left); + this.right.sync(right); + for (const p of mount) { + p.dom.classList.add("cm-panel"); + if (p.mount) p.mount(); + } + } else { + for (const p of this.panels) if (p.update) p.update(update); + } + } + + /** + * Destroy panel + */ + destroy() { + this.left.sync([]); + this.right.sync([]); + } +}, { + // provide: PluginField.scrollMargins.from(value => ({left: value.left.scrollMargin(), right: value.right.scrollMargin()})) +}); + +/** + * PanelGroup + */ +class PanelGroup { + + /** + * @param {EditorView} view + * @param {boolean} left + * @param {HTMLElement} container + */ + constructor(view, left, container) { + this.view = view; + this.left = left; + this.container = container; + this.dom = undefined; + this.classes = ""; + this.panels = []; + this.bufferWidth = 0; + this.syncClasses(); + } + + /** + * @param {Panel[]} panels + */ + sync(panels) { + for (const p of this.panels) if (p.destroy && panels.indexOf(p) < 0) p.destroy(); + this.panels = panels; + this.syncDOM(); + } + + /** + * Synchronise the DOM + */ + syncDOM() { + if (this.panels.length === 0) { + if (this.dom) { + this.dom.remove(); + this.dom = undefined; + } + return; + } + + const parent = this.container || this.view.dom; + if (!this.dom) { + this.dom = document.createElement("div"); + this.dom.className = this.left ? "cm-side-panels cm-panels-left" : "cm-side-panels cm-panels-right"; + parent.insertBefore(this.dom, parent.firstChild); + } + + let curDOM = this.dom.firstChild; + for (const panel of this.panels) { + if (panel.dom.parentNode === this.dom) { + while (curDOM !== panel.dom) curDOM = rm(curDOM); + curDOM = curDOM.nextSibling; + } else { + this.dom.insertBefore(panel.dom, curDOM); + this.bufferWidth = panel.width; + panel.dom.style.width = panel.width + "px"; + this.dom.style.width = this.bufferWidth + "px"; + } + } + while (curDOM) curDOM = rm(curDOM); + + const margin = this.left ? "marginLeft" : "marginRight"; + parent.querySelector(".cm-scroller").style[margin] = this.bufferWidth + "px"; + } + + /** + * + */ + scrollMargin() { + return !this.dom || this.container ? 0 : + Math.max(0, this.left ? + this.dom.getBoundingClientRect().right - Math.max(0, this.view.scrollDOM.getBoundingClientRect().left) : + Math.min(innerHeight, this.view.scrollDOM.getBoundingClientRect().right) - this.dom.getBoundingClientRect().left); + } + + /** + * + */ + syncClasses() { + if (!this.container || this.classes === this.view.themeClasses) return; + for (const cls of this.classes.split(" ")) if (cls) this.container.classList.remove(cls); + for (const cls of (this.classes = this.view.themeClasses).split(" ")) if (cls) this.container.classList.add(cls); + } +} + +/** + * @param {ChildNode} node + * @returns HTMLElement + */ +function rm(node) { + const next = node.nextSibling; + node.remove(); + return next; +} + +const baseTheme = EditorView.baseTheme({ + ".cm-side-panels": { + boxSizing: "border-box", + position: "absolute", + height: "100%", + top: 0, + bottom: 0 + }, + "&light .cm-side-panels": { + backgroundColor: "#f5f5f5", + color: "black" + }, + "&light .cm-panels-left": { + borderRight: "1px solid #ddd", + left: 0 + }, + "&light .cm-panels-right": { + borderLeft: "1px solid #ddd", + right: 0 + }, + "&dark .cm-side-panels": { + backgroundColor: "#333338", + color: "white" + } +}); + +/** + * Opening a panel is done by providing a constructor function for + * the panel through this facet. (The panel is closed again when its + * constructor is no longer provided.) Values of `null` are ignored. + */ +export const showSidePanel = Facet.define({ + enables: [panelPlugin, baseTheme] +}); diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index caa1a098..000940a4 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -9,7 +9,6 @@ import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWork import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs"; import Utils, {debounce} from "../../core/Utils.mjs"; import {toBase64} from "../../core/lib/Base64.mjs"; -import {isImage} from "../../core/lib/FileType.mjs"; import cptable from "codepage"; import { @@ -21,6 +20,7 @@ import {bracketMatching} from "@codemirror/language"; import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search"; import {statusBar} from "../utils/statusBar.mjs"; +import {fileDetailsPanel} from "../utils/fileDetails.mjs"; import {renderSpecialChar} from "../utils/editorUtils.mjs"; @@ -65,7 +65,8 @@ class InputWaiter { initEditor() { this.inputEditorConf = { eol: new Compartment, - lineWrapping: new Compartment + lineWrapping: new Compartment, + fileDetailsPanel: new Compartment }; const initialState = EditorState.create({ @@ -92,6 +93,7 @@ class InputWaiter { }), // Mutable state + this.inputEditorConf.fileDetailsPanel.of([]), this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping), this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")), @@ -466,43 +468,32 @@ class InputWaiter { if (inputNum !== activeTab) return; if (inputData.file) { - this.setFile(inputNum, inputData, silent); + this.setFile(inputNum, inputData); } else { - // TODO Per-tab encodings? - let inputVal; - if (this.inputChrEnc > 0) { - inputVal = cptable.utils.decode(this.inputChrEnc, new Uint8Array(inputData.buffer)); - } else { - inputVal = Utils.arrayBufferToStr(inputData.buffer); - } - - this.setInput(inputVal); - const fileOverlay = document.getElementById("input-file"), - fileName = document.getElementById("input-file-name"), - fileSize = document.getElementById("input-file-size"), - fileType = document.getElementById("input-file-type"), - fileLoaded = document.getElementById("input-file-loaded"); - - fileOverlay.style.display = "none"; - fileName.textContent = ""; - fileSize.textContent = ""; - fileType.textContent = ""; - fileLoaded.textContent = ""; - - this.inputTextEl.classList.remove("blur"); - - // Set URL to current input - if (inputVal.length >= 0 && inputVal.length <= 51200) { - const inputStr = toBase64(inputVal, "A-Za-z0-9+/"); - this.setUrl({ - includeInput: true, - input: inputStr - }); - } - - if (!silent) window.dispatchEvent(this.manager.statechange); + this.clearFile(inputNum); } + // TODO Per-tab encodings? + let inputVal; + if (this.inputChrEnc > 0) { + inputVal = cptable.utils.decode(this.inputChrEnc, new Uint8Array(inputData.buffer)); + } else { + inputVal = Utils.arrayBufferToStr(inputData.buffer); + } + + this.setInput(inputVal); + + // Set URL to current input + if (inputVal.length >= 0 && inputVal.length <= 51200) { + const inputStr = toBase64(inputVal, "A-Za-z0-9+/"); + this.setUrl({ + includeInput: true, + input: inputStr + }); + } + + if (!silent) window.dispatchEvent(this.manager.statechange); + }.bind(this)); } @@ -520,33 +511,38 @@ class InputWaiter { * @param {string} file.type * @param {string} status * @param {number} progress - * @param {boolean} [silent=true] - If false, fires the manager statechange event */ - setFile(inputNum, inputData, silent=true) { + setFile(inputNum, inputData) { const activeTab = this.manager.tabs.getActiveInputTab(); if (inputNum !== activeTab) return; - const fileOverlay = document.getElementById("input-file"), - fileName = document.getElementById("input-file-name"), - fileSize = document.getElementById("input-file-size"), - fileType = document.getElementById("input-file-type"), - fileLoaded = document.getElementById("input-file-loaded"); + // Create file details panel + this.inputEditorView.dispatch({ + effects: this.inputEditorConf.fileDetailsPanel.reconfigure( + fileDetailsPanel({ + fileDetails: inputData.file, + progress: inputData.progress, + status: inputData.status, + buffer: inputData.buffer, + renderPreview: this.app.options.imagePreview + }) + ) + }); + } - fileOverlay.style.display = "block"; - fileName.textContent = inputData.file.name; - fileSize.textContent = inputData.file.size + " bytes"; - fileType.textContent = inputData.file.type; - if (inputData.status === "error") { - fileLoaded.textContent = "Error"; - fileLoaded.style.color = "#FF0000"; - } else { - fileLoaded.style.color = ""; - fileLoaded.textContent = inputData.progress + "%"; - } + /** + * Clears the file details panel + * + * @param {number} inputNum + */ + clearFile(inputNum) { + const activeTab = this.manager.tabs.getActiveInputTab(); + if (inputNum !== activeTab) return; - this.displayFilePreview(inputNum, inputData); - - if (!silent) window.dispatchEvent(this.manager.statechange); + // Clear file details panel + this.inputEditorView.dispatch({ + effects: this.inputEditorConf.fileDetailsPanel.reconfigure([]) + }); } /** @@ -571,60 +567,6 @@ class InputWaiter { this.updateFileProgress(inputNum, 100); } - /** - * Render the input thumbnail - */ - async renderFileThumb() { - const activeTab = this.manager.tabs.getActiveInputTab(), - input = await this.getInputValue(activeTab), - fileThumb = document.getElementById("input-file-thumbnail"); - - if (typeof input === "string" || - !this.app.options.imagePreview) { - this.resetFileThumb(); - return; - } - - const inputArr = new Uint8Array(input), - type = isImage(inputArr); - - if (type && type !== "image/tiff" && inputArr.byteLength <= 512000) { - // Most browsers don't support displaying TIFFs, so ignore them - // Don't render images over 512000 bytes - const blob = new Blob([inputArr], {type: type}), - url = URL.createObjectURL(blob); - fileThumb.src = url; - } else { - this.resetFileThumb(); - } - - } - - /** - * Reset the input thumbnail to the default icon - */ - resetFileThumb() { - const fileThumb = document.getElementById("input-file-thumbnail"); - fileThumb.src = require("../static/images/file-128x128.png").default; - } - - /** - * Shows a chunk of the file in the input behind the file overlay - * - * @param {number} inputNum - The inputNum of the file being displayed - * @param {Object} inputData - Object containing the input data - * @param {string} inputData.stringSample - The first 4096 bytes of input as a string - */ - displayFilePreview(inputNum, inputData) { - const activeTab = this.manager.tabs.getActiveInputTab(), - input = inputData.buffer; - if (inputNum !== activeTab) return; - this.inputTextEl.classList.add("blur"); - this.setInput(input.stringSample); - - this.renderFileThumb(); - } - /** * Updates the displayed load progress for a file * @@ -632,17 +574,19 @@ class InputWaiter { * @param {number | string} progress - Either a number or "error" */ updateFileProgress(inputNum, progress) { - const activeTab = this.manager.tabs.getActiveInputTab(); - if (inputNum !== activeTab) return; + // const activeTab = this.manager.tabs.getActiveInputTab(); + // if (inputNum !== activeTab) return; - const fileLoaded = document.getElementById("input-file-loaded"); - if (progress === "error") { - fileLoaded.textContent = "Error"; - fileLoaded.style.color = "#FF0000"; - } else { - fileLoaded.textContent = progress + "%"; - fileLoaded.style.color = ""; - } + // TODO + + // const fileLoaded = document.getElementById("input-file-loaded"); + // if (progress === "error") { + // fileLoaded.textContent = "Error"; + // fileLoaded.style.color = "#FF0000"; + // } else { + // fileLoaded.textContent = progress + "%"; + // fileLoaded.style.color = ""; + // } } /** @@ -778,10 +722,6 @@ class InputWaiter { */ inputChange(e) { debounce(function(e) { - // Ignore this function if the input is a file - const fileOverlay = document.getElementById("input-file"); - if (fileOverlay.style.display === "block") return; - const value = this.getInput(); const activeTab = this.manager.tabs.getActiveInputTab(); @@ -806,7 +746,7 @@ class InputWaiter { e.stopPropagation(); e.preventDefault(); - e.target.closest("#input-text,#input-file").classList.add("dropping-file"); + e.target.closest("#input-text").classList.add("dropping-file"); } /** @@ -821,7 +761,7 @@ class InputWaiter { // Dragleave often fires when moving between lines in the editor. // If the target element is within the input-text element, we are still on target. if (!this.inputTextEl.contains(e.target)) - e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); + e.target.closest("#input-text").classList.remove("dropping-file"); } /** @@ -837,7 +777,7 @@ class InputWaiter { e.stopPropagation(); e.preventDefault(); - e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); + e.target.closest("#input-text").classList.remove("dropping-file"); // Dropped text is handled by the editor itself if (e.dataTransfer.getData("Text")) return; @@ -1063,23 +1003,6 @@ class InputWaiter { window.dispatchEvent(this.manager.statechange); } - /** - * Handler for clear IO click event. - * Resets the input for the current tab - */ - clearIoClick() { - const inputNum = this.manager.tabs.getActiveInputTab(); - if (inputNum === -1) return; - - this.updateInputValue(inputNum, "", true); - - this.set(inputNum, { - buffer: new ArrayBuffer() - }); - - this.manager.tabs.updateInputTabHeader(inputNum, ""); - } - /** * Sets the console log level in the worker. * From 406da9fa2c8bc5b40e16b9dbb7251966f03a413c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 2 Sep 2022 20:15:07 +0100 Subject: [PATCH 207/686] Efficiency improvements to reduce unnecessary casting --- src/core/Utils.mjs | 9 +++++- src/web/waiters/InputWaiter.mjs | 1 - src/web/waiters/OutputWaiter.mjs | 47 ++++++++++++++++---------------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 604b7b8c..fec3b9be 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -407,6 +407,7 @@ class Utils { */ static strToArrayBuffer(str) { log.debug("Converting string to array buffer"); + if (!str) return new ArrayBuffer; const arr = new Uint8Array(str.length); let i = str.length, b; while (i--) { @@ -434,6 +435,7 @@ class Utils { */ static strToUtf8ArrayBuffer(str) { log.debug("Converting string to UTF8 array buffer"); + if (!str) return new ArrayBuffer; const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { @@ -464,6 +466,7 @@ class Utils { */ static strToByteArray(str) { log.debug("Converting string to byte array"); + if (!str) return []; const byteArray = new Array(str.length); let i = str.length, b; while (i--) { @@ -491,6 +494,7 @@ class Utils { */ static strToUtf8ByteArray(str) { log.debug("Converting string to UTF8 byte array"); + if (!str) return []; const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { @@ -520,6 +524,7 @@ class Utils { */ static strToCharcode(str) { log.debug("Converting string to charcode"); + if (!str) return []; const charcode = []; for (let i = 0; i < str.length; i++) { @@ -555,6 +560,7 @@ class Utils { */ static byteArrayToUtf8(byteArray) { log.debug("Converting byte array to UTF8"); + if (!byteArray || !byteArray.length) return ""; const str = Utils.byteArrayToChars(byteArray); try { const utf8Str = utf8.decode(str); @@ -588,7 +594,7 @@ class Utils { */ static byteArrayToChars(byteArray) { log.debug("Converting byte array to chars"); - if (!byteArray) return ""; + if (!byteArray || !byteArray.length) return ""; let str = ""; // String concatenation appears to be faster than an array join for (let i = 0; i < byteArray.length;) { @@ -611,6 +617,7 @@ class Utils { */ static arrayBufferToStr(arrayBuffer, utf8=true) { log.debug("Converting array buffer to str"); + if (!arrayBuffer || !arrayBuffer.byteLength) return ""; const arr = new Uint8Array(arrayBuffer); return utf8 ? Utils.byteArrayToUtf8(arr) : Utils.byteArrayToChars(arr); } diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 000940a4..86ad9873 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -997,7 +997,6 @@ class InputWaiter { this.setupInputWorker(); this.manager.worker.setupChefWorker(); this.addInput(true); - this.bakeAll(); // Fire the statechange event as the input has been modified window.dispatchEvent(this.manager.statechange); diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index f0b03d72..a247375e 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -49,6 +49,8 @@ class OutputWaiter { html: "", changed: false }; + // Hold a copy of the currently displayed output so that we don't have to update it unnecessarily + this.currentOutputCache = null; this.outputChrEnc = 0; this.initEditor(); @@ -170,9 +172,26 @@ class OutputWaiter { /** * Sets the value of the current output - * @param {string} data + * @param {string|ArrayBuffer} data */ setOutput(data) { + // Don't do anything if the output hasn't changed + if (data === this.currentOutputCache) return; + this.currentOutputCache = data; + + // If data is an ArrayBuffer, convert to a string in the correct character encoding + if (data instanceof ArrayBuffer) { + if (this.outputChrEnc === 0) { + data = Utils.arrayBufferToStr(data); + } else { + try { + data = cptable.utils.decode(this.outputChrEnc, new Uint8Array(data)); + } catch (err) { + data = err; + } + } + } + // Turn drawSelection back on this.outputEditorView.dispatch({ effects: this.outputEditorConf.drawSelection.reconfigure( @@ -508,28 +527,7 @@ class OutputWaiter { this.setHTMLOutput(output.data.result); break; - case "ArrayBuffer": { - this.outputTextEl.style.display = "block"; - outputFile.style.display = "none"; - - this.clearHTMLOutput(); - - let outputVal = ""; - if (this.outputChrEnc === 0) { - outputVal = Utils.arrayBufferToStr(output.data.result); - } else { - try { - outputVal = cptable.utils.decode(this.outputChrEnc, new Uint8Array(output.data.result)); - } catch (err) { - outputVal = err; - } - } - - this.setOutput(outputVal); - - // this.setFile(await this.getDishBuffer(output.data.dish), activeTab); - break; - } + case "ArrayBuffer": case "string": default: this.outputTextEl.style.display = "block"; @@ -1136,7 +1134,8 @@ class OutputWaiter { * @param {number} inputNum */ async displayTabInfo(inputNum) { - if (!this.outputExists(inputNum)) return; + // Don't display anything if there are no, or only one, tabs + if (!this.outputExists(inputNum) || Object.keys(this.outputs).length <= 1) return; const dish = this.getOutputDish(inputNum); let tabStr = ""; From 65d883496bc3fc8c214e27542e3378ff554e1fd5 Mon Sep 17 00:00:00 2001 From: IsSafrullah Date: Tue, 6 Sep 2022 03:52:42 +0700 Subject: [PATCH 208/686] fix select when change theme --- src/web/stylesheets/utils/_overrides.css | 27 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/web/stylesheets/utils/_overrides.css b/src/web/stylesheets/utils/_overrides.css index c06d3b8c..e1c36c12 100755 --- a/src/web/stylesheets/utils/_overrides.css +++ b/src/web/stylesheets/utils/_overrides.css @@ -82,7 +82,17 @@ a:focus { border-color: var(--btn-success-hover-border-colour); } -select.form-control:not([size]):not([multiple]), select.custom-file-control:not([size]):not([multiple]) { +select.form-control, +select.form-control:focus { + background-color: var(--primary-background-colour) !important; +} + +select.form-control:focus { + transition: none !important; +} + +select.form-control:not([size]):not([multiple]), +select.custom-file-control:not([size]):not([multiple]) { height: unset !important; } @@ -145,7 +155,8 @@ optgroup { color: var(--primary-font-colour); } -.table-bordered th, .table-bordered td { +.table-bordered th, +.table-bordered td { border: 1px solid var(--table-border-colour); } @@ -172,7 +183,9 @@ optgroup { color: var(--subtext-font-colour); } -.nav-tabs>li>a.nav-link.active, .nav-tabs>li>a.nav-link.active:focus, .nav-tabs>li>a.nav-link.active:hover { +.nav-tabs>li>a.nav-link.active, +.nav-tabs>li>a.nav-link.active:focus, +.nav-tabs>li>a.nav-link.active:hover { background-color: var(--secondary-background-colour); border-color: var(--secondary-border-colour); border-bottom-color: transparent; @@ -183,7 +196,8 @@ optgroup { border-color: var(--primary-border-colour); } -.nav a.nav-link:focus, .nav a.nav-link:hover { +.nav a.nav-link:focus, +.nav a.nav-link:hover { background-color: var(--secondary-border-colour); } @@ -199,7 +213,8 @@ optgroup { color: var(--primary-font-colour); } -.dropdown-menu a:focus, .dropdown-menu a:hover { +.dropdown-menu a:focus, +.dropdown-menu a:hover { background-color: var(--secondary-background-colour); color: var(--primary-font-colour); } @@ -231,4 +246,4 @@ optgroup { .colorpicker-color, .colorpicker-color div { height: 100px; -} +} \ No newline at end of file From 3893c22275142774cd32d7f946a019ea130e35e0 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Sep 2022 16:35:21 +0100 Subject: [PATCH 209/686] Changing the output encoding no longer triggers a full bake --- src/web/utils/statusBar.mjs | 12 +++++++++--- src/web/waiters/OutputWaiter.mjs | 7 +++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs index f9be5006..efabea81 100644 --- a/src/web/utils/statusBar.mjs +++ b/src/web/utils/statusBar.mjs @@ -81,6 +81,9 @@ class StatusBarPanel { * @param {Event} e */ eolSelectClick(e) { + // preventDefault is required to stop the URL being modified and popState being triggered + e.preventDefault(); + const eolLookup = { "LF": "\u000a", "VT": "\u000b", @@ -106,6 +109,9 @@ class StatusBarPanel { * @param {Event} e */ chrEncSelectClick(e) { + // preventDefault is required to stop the URL being modified and popState being triggered + e.preventDefault(); // TODO - this breaks the menus when you click the button itself + const chrEncVal = parseInt(e.target.getAttribute("data-val"), 10); if (isNaN(chrEncVal)) return; @@ -366,9 +372,9 @@ function hideOnClickOutside(element, instantiatingEvent) { } }; - if (!Object.keys(elementsWithListeners).includes(element)) { - document.addEventListener("click", outsideClickListener); + if (!Object.prototype.hasOwnProperty.call(elementsWithListeners, element)) { elementsWithListeners[element] = outsideClickListener; + document.addEventListener("click", elementsWithListeners[element], false); } } @@ -378,7 +384,7 @@ function hideOnClickOutside(element, instantiatingEvent) { */ function hideElement(element) { element.classList.remove("show"); - document.removeEventListener("click", elementsWithListeners[element]); + document.removeEventListener("click", elementsWithListeners[element], false); delete elementsWithListeners[element]; } diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs index a247375e..f1965c77 100755 --- a/src/web/waiters/OutputWaiter.mjs +++ b/src/web/waiters/OutputWaiter.mjs @@ -146,6 +146,8 @@ class OutputWaiter { */ chrEncChange(chrEncVal) { this.outputChrEnc = chrEncVal; + // Reset the output, forcing it to re-decode the data with the new character encoding + this.setOutput(this.currentOutputCache, true); } /** @@ -173,10 +175,11 @@ class OutputWaiter { /** * Sets the value of the current output * @param {string|ArrayBuffer} data + * @param {boolean} [force=false] */ - setOutput(data) { + setOutput(data, force=false) { // Don't do anything if the output hasn't changed - if (data === this.currentOutputCache) return; + if (!force && data === this.currentOutputCache) return; this.currentOutputCache = data; // If data is an ArrayBuffer, convert to a string in the correct character encoding From 86b43b4ffae14d9b85935fa9dc7e6ee0d30a1c2f Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Sep 2022 16:39:10 +0100 Subject: [PATCH 210/686] Updated README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 021e3515..48811566 100755 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ You can use as many operations as you like in simple or complex ways. Some examp By manipulating CyberChef's URL hash, you can change the initial settings with which the page opens. The format is `https://gchq.github.io/CyberChef/#recipe=Operation()&input=...` -Supported arguments are `recipe`, `input` (encoded in Base64), and `theme`. +Supported arguments are `recipe`, `input` (encoded in Base64), and `theme`. ## Browser support @@ -90,7 +90,7 @@ CyberChef is built to support ## Node.js support -CyberChef is built to fully support Node.js `v10` and partially supports `v12`. Named imports using a deep import specifier do not work in `v12`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki) +CyberChef is built to fully support Node.js `v16`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki) ## Contributing From cef7a7b27d6e8fca45f314eef15516ea183a9e2c Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Sep 2022 16:44:41 +0100 Subject: [PATCH 211/686] Lint --- src/web/stylesheets/utils/_overrides.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/stylesheets/utils/_overrides.css b/src/web/stylesheets/utils/_overrides.css index e1c36c12..7deabe7d 100755 --- a/src/web/stylesheets/utils/_overrides.css +++ b/src/web/stylesheets/utils/_overrides.css @@ -246,4 +246,4 @@ optgroup { .colorpicker-color, .colorpicker-color div { height: 100px; -} \ No newline at end of file +} From d90d845f27273c28a4f590401a3c0ed15437d827 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Sep 2022 16:51:38 +0100 Subject: [PATCH 212/686] 9.46.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1712692..3cdc4234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "9.46.0", + "version": "9.46.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "9.46.0", + "version": "9.46.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 48d6f693..b45d9b25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.46.0", + "version": "9.46.1", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 1dd1b839b8fc6b446d589afa2bbdc3b6bf1b2af0 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 9 Sep 2022 20:39:28 +0100 Subject: [PATCH 213/686] Switched jsonpath library to jsonpath-plus. Fixes #1318 --- package-lock.json | 135 ++---------------------- package.json | 2 +- src/core/operations/JPathExpression.mjs | 33 +++--- tests/operations/tests/Code.mjs | 35 ++++-- 4 files changed, 57 insertions(+), 148 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cdc4234..c9d63374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "js-sha3": "^0.8.0", "jsesc": "^3.0.2", "json5": "^2.2.1", - "jsonpath": "^1.1.1", + "jsonpath-plus": "^7.2.0", "jsonwebtoken": "^8.5.1", "jsqr": "^1.4.0", "jsrsasign": "^10.5.23", @@ -9498,26 +9498,12 @@ "node": ">=6" } }, - "node_modules/jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", - "dependencies": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" - } - }, - "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", "engines": { - "node": ">=0.4.0" + "node": ">=12.0.0" } }, "node_modules/jsonwebtoken": { @@ -14055,52 +14041,6 @@ "node": ">=8" } }, - "node_modules/static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "dependencies": { - "escodegen": "^1.8.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -14767,11 +14707,6 @@ "node": ">=0.10.0" } }, - "node_modules/underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" - }, "node_modules/underscore.string": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", @@ -23025,22 +22960,10 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, - "jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", - "requires": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" - }, - "dependencies": { - "esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=" - } - } + "jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==" }, "jsonwebtoken": { "version": "8.5.1", @@ -26583,39 +26506,6 @@ } } }, - "static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "requires": { - "escodegen": "^1.8.1" - }, - "dependencies": { - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -27126,11 +27016,6 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, - "underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" - }, "underscore.string": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", diff --git a/package.json b/package.json index b45d9b25..c1b60b18 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "js-sha3": "^0.8.0", "jsesc": "^3.0.2", "json5": "^2.2.1", - "jsonpath": "^1.1.1", + "jsonpath-plus": "^7.2.0", "jsonwebtoken": "^8.5.1", "jsqr": "^1.4.0", "jsrsasign": "^10.5.23", diff --git a/src/core/operations/JPathExpression.mjs b/src/core/operations/JPathExpression.mjs index 328fc83f..73a27433 100644 --- a/src/core/operations/JPathExpression.mjs +++ b/src/core/operations/JPathExpression.mjs @@ -4,7 +4,7 @@ * @license Apache-2.0 */ -import jpath from "jsonpath"; +import {JSONPath} from "jsonpath-plus"; import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; @@ -27,14 +27,20 @@ class JPathExpression extends Operation { this.outputType = "string"; this.args = [ { - "name": "Query", - "type": "string", - "value": "" + name: "Query", + type: "string", + value: "" }, { - "name": "Result delimiter", - "type": "binaryShortString", - "value": "\\n" + name: "Result delimiter", + type: "binaryShortString", + value: "\\n" + }, + { + name: "Prevent eval", + type: "boolean", + value: true, + description: "Evaluated expressions are disabled by default for security reasons" } ]; } @@ -45,18 +51,21 @@ class JPathExpression extends Operation { * @returns {string} */ run(input, args) { - const [query, delimiter] = args; - let results, - obj; + const [query, delimiter, preventEval] = args; + let results, jsonObj; try { - obj = JSON.parse(input); + jsonObj = JSON.parse(input); } catch (err) { throw new OperationError(`Invalid input JSON: ${err.message}`); } try { - results = jpath.query(obj, query); + results = JSONPath({ + path: query, + json: jsonObj, + preventEval: preventEval + }); } catch (err) { throw new OperationError(`Invalid JPath expression: ${err.message}`); } diff --git a/tests/operations/tests/Code.mjs b/tests/operations/tests/Code.mjs index 94179553..6ff1d97c 100644 --- a/tests/operations/tests/Code.mjs +++ b/tests/operations/tests/Code.mjs @@ -185,11 +185,11 @@ TestRegister.addTests([ { name: "JPath Expression: Empty expression", input: JSON.stringify(JSON_TEST_DATA), - expectedOutput: "Invalid JPath expression: we need a path", + expectedOutput: "", recipeConfig: [ { "op": "JPath expression", - "args": ["", "\n"] + "args": ["", "\n", true] } ], }, @@ -205,7 +205,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$.store.book[*].author", "\n"] + "args": ["$.store.book[*].author", "\n", true] } ], }, @@ -223,7 +223,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..title", "\n"] + "args": ["$..title", "\n", true] } ], }, @@ -238,7 +238,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$.store.*", "\n"] + "args": ["$.store.*", "\n", true] } ], }, @@ -249,7 +249,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..book[-1:]", "\n"] + "args": ["$..book[-1:]", "\n", true] } ], }, @@ -263,7 +263,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..book[:2]", "\n"] + "args": ["$..book[:2]", "\n", true] } ], }, @@ -277,7 +277,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..book[?(@.isbn)]", "\n"] + "args": ["$..book[?(@.isbn)]", "\n", false] } ], }, @@ -292,7 +292,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..book[?(@.price<30 && @.category==\"fiction\")]", "\n"] + "args": ["$..book[?(@.price<30 && @.category==\"fiction\")]", "\n", false] } ], }, @@ -306,10 +306,25 @@ TestRegister.addTests([ recipeConfig: [ { "op": "JPath expression", - "args": ["$..book[?(@.price<10)]", "\n"] + "args": ["$..book[?(@.price<10)]", "\n", false] } ], }, + { + name: "JPath Expression: Script-based expression", + input: "[{}]", + recipeConfig: [ + { + "op": "JPath expression", + "args": [ + "$..[?(({__proto__:[].constructor}).constructor(\"self.postMessage({action:'bakeComplete',data:{bakeId:1,dish:{type:1,value:''},duration:1,error:false,id:undefined,inputNum:2,progress:1,result:'