From 1f09c03d4897206e7ed4a3d90cac6c577c486aed Mon Sep 17 00:00:00 2001 From: GCHQ 77703 Date: Fri, 15 Feb 2019 14:23:16 +0000 Subject: [PATCH 001/630] 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/630] 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/630] 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/630] 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/630] 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/630] 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/630] 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/630] 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/630] 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 e92ed13864d5e404fa9986e6972b61ca83a7123a Mon Sep 17 00:00:00 2001 From: n1073645 Date: Thu, 21 Nov 2019 12:53:44 +0000 Subject: [PATCH 010/630] 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 011/630] 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 012/630] 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 013/630] 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 014/630] 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 015/630] 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 016/630] 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 017/630] 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 018/630] 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 019/630] 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 020/630] 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 021/630] 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 022/630] 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 023/630] 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 024/630] 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 025/630] 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 026/630] 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 027/630] 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 028/630] 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 029/630] 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 030/630] 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 031/630] 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 032/630] 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 033/630] 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 034/630] 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 035/630] 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 036/630] 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 037/630] 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 038/630] 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 039/630] 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 040/630] 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 041/630] 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 042/630] 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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

>*gmXw_*^lG!e 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›X6oq95 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 literal 0 HcmV?d00001 diff --git a/src/web/static/fonts/MaterialIcons-Regular.woff2 b/src/web/static/fonts/MaterialIcons-Regular.woff2 deleted file mode 100644 index 9fa211252080046a23b2449dbdced6abc2b0bb34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44300 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 QW8}wPUeo^@jR61v08@RD2LJ#7 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 179/630] 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 180/630] 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 181/630] 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 182/630] 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 183/630] 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 184/630] 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 185/630] 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 186/630] 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 187/630] 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 188/630] 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 189/630] 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 190/630] 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 191/630] 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 192/630] 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 193/630] 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 194/630] 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 195/630] 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 196/630] 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 197/630] 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 198/630] 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 199/630] 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 200/630] 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 201/630] 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 202/630] 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 203/630] 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 204/630] 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 205/630] 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 206/630] 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 207/630] 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 208/630] 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 209/630] 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 210/630] 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 211/630] 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 212/630] 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 213/630] 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 214/630] 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 215/630] 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 216/630] 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 217/630] 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 218/630] 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 219/630] 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 220/630] 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 221/630] 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 222/630] 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 223/630] 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 224/630] 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 225/630] 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 226/630] 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 227/630] 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 228/630] 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 229/630] 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:'