diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index a068ffbb..a77f4984 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -25,6 +25,7 @@ jobs: - name: Install run: | + export DETECT_CHROMEDRIVER_VERSION=true npm ci npm run setheapsize @@ -61,12 +62,22 @@ jobs: tags: ${{ steps.image-metadata.outputs.tags }} labels: ${{ steps.image-metadata.outputs.labels }} containerfiles: ./Dockerfile - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 oci: true + # enable build layer caching between platforms + layers: true # Webpack seems to use a lot of open files, increase the max open file limit to accomodate. extra-args: | --ulimit nofile=10000 + - name: Publish to GHCR + uses: redhat-actions/push-to-registry@v2 + with: + image: ${{ steps.build-image.outputs.image }} + tags: ${{ steps.build-image.outputs.tags }} + registry: ${{ env.REGISTRY }} + username: ${{ env.REGISTRY_USER }} + password: ${{ env.REGISTRY_PASSWORD }} - name: Upload Release Assets id: upload-release-assets @@ -83,11 +94,3 @@ jobs: uses: JS-DevTools/npm-publish@v1 with: token: ${{ secrets.NPM_TOKEN }} - - - name: Publish to GHCR - uses: redhat-actions/push-to-registry@v2 - with: - tags: ${{ steps.build-image.outputs.tags }} - registry: ${{ env.REGISTRY }} - username: ${{ env.REGISTRY_USER }} - password: ${{ env.REGISTRY_PASSWORD }} diff --git a/Dockerfile b/Dockerfile index be4c8bad..ba605fd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,35 @@ -FROM node:18-alpine AS build +##################################### +# Build the app to a static website # +##################################### +# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds +# This is because npm "chromedriver" package is not compatiable with all platforms +# For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation +FROM --platform=$BUILDPLATFORM node:18-alpine AS builder +WORKDIR /app + +COPY package.json . +COPY package-lock.json . + +# Install dependencies +# --ignore-scripts prevents postinstall script (which runs grunt) as it depends on files other than package.json +RUN npm ci --ignore-scripts + +# Copy files needed for postinstall and build COPY . . -RUN npm ci + +# npm postinstall runs grunt, which depends on files other than package.json +RUN npm run postinstall + +# Build the app RUN npm run build -FROM nginx:1.25-alpine3.18 AS cyberchef +######################################### +# Package static build files into nginx # +######################################### +# We are using Github Actions: redhat-actions/buildah-build@v2 which needs manual selection of arch in base image +# Remove TARGETARCH if docker buildx is supported in the CI release as --platform=$TARGETPLATFORM will be automatically set +ARG TARGETPLATFORM +FROM --platform=${TARGETPLATFORM} nginx:stable-alpine AS cyberchef -COPY --from=build ./build/prod /usr/share/nginx/html/ +COPY --from=builder /app/build/prod /usr/share/nginx/html/ diff --git a/package-lock.json b/package-lock.json index adc9c018..e9fc811a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^5.2.0", + "dompurify": "^3.2.5", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -45,6 +46,7 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "hash-wasm": "^4.12.0", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", @@ -53,6 +55,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.0.2", "json5": "^2.2.3", + "jsonata": "^2.0.3", "jsonpath-plus": "^9.0.0", "jsonwebtoken": "8.5.1", "jsqr": "^1.4.0", @@ -4365,6 +4368,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -8411,6 +8421,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", + "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -10924,6 +10943,11 @@ "node": ">= 0.10" } }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -12448,6 +12472,14 @@ "node": ">=6" } }, + "node_modules/jsonata": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", + "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonpath-plus": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-9.0.0.tgz", diff --git a/package.json b/package.json index 43655124..5b937017 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^5.2.0", + "dompurify": "^3.2.5", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -131,6 +132,7 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "hash-wasm": "^4.12.0", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", @@ -139,6 +141,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.0.2", "json5": "^2.2.3", + "jsonata": "^2.0.3", "jsonpath-plus": "^9.0.0", "jsonwebtoken": "8.5.1", "jsqr": "^1.4.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6c5e7fc0..d6d3d91b 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -294,6 +294,7 @@ "To Upper case", "To Lower case", "Swap case", + "Alternating Caps", "To Case Insensitive Regex", "From Case Insensitive Regex", "Add line numbers", @@ -369,6 +370,7 @@ "Regular expression", "XPath expression", "JPath expression", + "Jsonata Query", "CSS selector", "Extract EXIF", "Extract ID3", @@ -404,6 +406,7 @@ "name": "Hashing", "ops": [ "Analyse hash", + "Generate all checksums", "Generate all hashes", "MD2", "MD4", @@ -422,6 +425,7 @@ "Snefru", "BLAKE2b", "BLAKE2s", + "BLAKE3", "GOST Hash", "Streebog", "SSDEEP", @@ -446,7 +450,8 @@ "Adler-32 Checksum", "Luhn Checksum", "CRC Checksum", - "TCP/IP Checksum" + "TCP/IP Checksum", + "XOR Checksum" ] }, { @@ -470,6 +475,7 @@ "Jq", "CSS selector", "PHP Deserialize", + "PHP Serialize", "Microsoft Script Decoder", "Strip HTML tags", "Diff", diff --git a/src/core/lib/Base32.mjs b/src/core/lib/Base32.mjs new file mode 100644 index 00000000..92b76eca --- /dev/null +++ b/src/core/lib/Base32.mjs @@ -0,0 +1,23 @@ +// import Utils from "../Utils.mjs"; + +/** + * Base32 resources. + * + * @author Peter C-S [petercs@purelymail.com] + * @license Apache-2.0 + */ + +/** + * Base32 alphabets. + */ +export const ALPHABET_OPTIONS = [ + { + name: "Standard", // https://www.rfc-editor.org/rfc/rfc4648#section-6 + value: "A-Z2-7=", + }, + { + name: "Hex Extended", // https://www.rfc-editor.org/rfc/rfc4648#section-7 + value: "0-9A-V=", + }, +]; + diff --git a/src/core/operations/AESDecrypt.mjs b/src/core/operations/AESDecrypt.mjs index e24a5119..5e6cec26 100644 --- a/src/core/operations/AESDecrypt.mjs +++ b/src/core/operations/AESDecrypt.mjs @@ -112,7 +112,7 @@ class AESDecrypt extends Operation { run(input, args) { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteString(args[1].string, args[1].option), - mode = args[2].substring(0, 3), + mode = args[2].split("/")[0], noPadding = args[2].endsWith("NoPadding"), inputType = args[3], outputType = args[4], diff --git a/src/core/operations/AESEncrypt.mjs b/src/core/operations/AESEncrypt.mjs index 7b52ff03..84e1c540 100644 --- a/src/core/operations/AESEncrypt.mjs +++ b/src/core/operations/AESEncrypt.mjs @@ -66,6 +66,14 @@ class AESEncrypt extends Operation { { name: "ECB", off: [5] + }, + { + name: "CBC/NoPadding", + off: [5] + }, + { + name: "ECB/NoPadding", + off: [5] } ] }, @@ -98,7 +106,8 @@ class AESEncrypt extends Operation { run(input, args) { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteString(args[1].string, args[1].option), - mode = args[2], + mode = args[2].split("/")[0], + noPadding = args[2].endsWith("NoPadding"), inputType = args[3], outputType = args[4], aad = Utils.convertToByteString(args[5].string, args[5].option); @@ -114,11 +123,20 @@ The following algorithms will be used based on the size of the key: input = Utils.convertToByteString(input, inputType); + // Handle NoPadding modes + if (noPadding && input.length % 16 !== 0) { + throw new OperationError("Input length must be a multiple of 16 bytes for NoPadding modes."); + } const cipher = forge.cipher.createCipher("AES-" + mode, key); cipher.start({ iv: iv, additionalData: mode === "GCM" ? aad : undefined }); + if (noPadding) { + cipher.mode.pad = function(output, options) { + return true; + }; + } cipher.update(forge.util.createBuffer(input)); cipher.finish(); diff --git a/src/core/operations/AlternatingCaps.mjs b/src/core/operations/AlternatingCaps.mjs new file mode 100644 index 00000000..2d54867c --- /dev/null +++ b/src/core/operations/AlternatingCaps.mjs @@ -0,0 +1,53 @@ +/** + * @author sw5678 + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Alternating caps operation + */ +class AlternatingCaps extends Operation { + + /** + * AlternatingCaps constructor + */ + constructor() { + super(); + + this.name = "Alternating Caps"; + this.module = "Default"; + this.description = "Alternating caps, also known as studly caps, sticky caps, or spongecase is a form of text notation in which the capitalization of letters varies by some pattern, or arbitrarily. An example of this would be spelling 'alternative caps' as 'aLtErNaTiNg CaPs'."; + this.infoURL = "https://en.wikipedia.org/wiki/Alternating_caps"; + this.inputType = "string"; + this.outputType = "string"; + this.args= []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let output = ""; + let previousCaps = true; + for (let i = 0; i < input.length; i++) { + // Check if the element is a letter + if (!RegExp(/^\p{L}/, "u").test(input[i])) { + output += input[i]; + } else if (previousCaps) { + output += input[i].toLowerCase(); + previousCaps = false; + } else { + output += input[i].toUpperCase(); + previousCaps = true; + } + } + return output; + } +} + +export default AlternatingCaps; diff --git a/src/core/operations/BLAKE3.mjs b/src/core/operations/BLAKE3.mjs new file mode 100644 index 00000000..0f686120 --- /dev/null +++ b/src/core/operations/BLAKE3.mjs @@ -0,0 +1,58 @@ +/** + * @author xumptex [xumptex@outlook.fr] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { blake3 } from "hash-wasm"; +/** + * BLAKE3 operation + */ +class BLAKE3 extends Operation { + + /** + * BLAKE3 constructor + */ + constructor() { + super(); + + this.name = "BLAKE3"; + this.module = "Hashing"; + this.description = "Hashes the input using BLAKE3 (UTF-8 encoded), with an optional key (also UTF-8), and outputs the result in hexadecimal format."; + this.infoURL = "https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Size (bytes)", + "type": "number" + }, { + "name": "Key", + "type": "string", + "value": "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = args[1]; + const size = args[0]; + // Check if the user want a key hash or not + if (key === "") { + return blake3(input, size*8); + } if (key.length !== 32) { + throw new OperationError("The key must be exactly 32 bytes long"); + } + return blake3(input, size*8, key); + } + +} + +export default BLAKE3; diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs index 7e46e867..1f8a53ea 100644 --- a/src/core/operations/ECDSAVerify.mjs +++ b/src/core/operations/ECDSAVerify.mjs @@ -9,6 +9,7 @@ import OperationError from "../errors/OperationError.mjs"; import { fromBase64 } from "../lib/Base64.mjs"; import { toHexFast } from "../lib/Hex.mjs"; import r from "jsrsasign"; +import Utils from "../Utils.mjs"; /** * ECDSA Verify operation @@ -59,6 +60,11 @@ class ECDSAVerify extends Operation { name: "Message", type: "text", value: "" + }, + { + name: "Message format", + type: "option", + value: ["Raw", "Hex", "Base64"] } ]; } @@ -70,7 +76,7 @@ class ECDSAVerify extends Operation { */ run(input, args) { let inputFormat = args[0]; - const [, mdAlgo, keyPem, msg] = args; + const [, mdAlgo, keyPem, msg, msgFormat] = args; if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) { throw new OperationError("Please enter a public key."); @@ -145,7 +151,8 @@ class ECDSAVerify extends Operation { throw new OperationError("Provided key is not a public key."); } sig.init(key); - sig.updateString(msg); + const messageStr = Utils.convertToByteString(msg, msgFormat); + sig.updateString(messageStr); const result = sig.verify(signatureASN1Hex); return result ? "Verified OK" : "Verification Failure"; } diff --git a/src/core/operations/ExtractEmailAddresses.mjs b/src/core/operations/ExtractEmailAddresses.mjs index f50e1aaf..34b838ab 100644 --- a/src/core/operations/ExtractEmailAddresses.mjs +++ b/src/core/operations/ExtractEmailAddresses.mjs @@ -51,7 +51,7 @@ class ExtractEmailAddresses extends Operation { run(input, args) { const [displayTotal, sort, unique] = args, // email regex from: https://www.regextester.com/98066 - regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}\])/ig; + regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig; const results = search( input, diff --git a/src/core/operations/ExtractIPAddresses.mjs b/src/core/operations/ExtractIPAddresses.mjs index 97b52478..b74ec8fe 100644 --- a/src/core/operations/ExtractIPAddresses.mjs +++ b/src/core/operations/ExtractIPAddresses.mjs @@ -21,7 +21,7 @@ class ExtractIPAddresses extends Operation { this.name = "Extract IP addresses"; this.module = "Regex"; - this.description = "Extracts all IPv4 and IPv6 addresses.

Warning: Given a string 710.65.0.456, this will match 10.65.0.45 so always check the original input!"; + this.description = "Extracts all IPv4 and IPv6 addresses.

Warning: Given a string 1.2.3.4.5.6.7.8, this will match 1.2.3.4 and 5.6.7.8 so always check the original input!"; this.inputType = "string"; this.outputType = "string"; this.args = [ @@ -65,7 +65,21 @@ class ExtractIPAddresses extends Operation { */ run(input, args) { const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args, - ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?", + + // IPv4 decimal groups can have values 0 to 255. To construct a regex the following sub-regex is reused: + ipv4DecimalByte = "(?:25[0-5]|2[0-4]\\d|1?[0-9]\\d|\\d)", + ipv4OctalByte = "(?:0[1-3]?[0-7]{1,2})", + + // Look behind and ahead will be used to exclude matches with additional decimal digits left and right of IP address + lookBehind = "(? { + const checksumLength = checksum.name.match(new RegExp("-(\\d{1,2})(\\/|$)"))[1]; + if (length === "All" || length === checksumLength) { + const value = checksum.algo.run(new Uint8Array(input), checksum.params || []); + output += includeNames ? + `${checksum.name}:${" ".repeat(25-checksum.name.length)}${value}\n`: + `${value}\n`; + } + }); + return output; + } +} + +export default GenerateAllChecksums; diff --git a/src/core/operations/GenerateAllHashes.mjs b/src/core/operations/GenerateAllHashes.mjs index 06b5f7d9..df09aa85 100644 --- a/src/core/operations/GenerateAllHashes.mjs +++ b/src/core/operations/GenerateAllHashes.mjs @@ -22,12 +22,6 @@ import HAS160 from "./HAS160.mjs"; import Whirlpool from "./Whirlpool.mjs"; import SSDEEP from "./SSDEEP.mjs"; import CTPH from "./CTPH.mjs"; -import Fletcher8Checksum from "./Fletcher8Checksum.mjs"; -import Fletcher16Checksum from "./Fletcher16Checksum.mjs"; -import Fletcher32Checksum from "./Fletcher32Checksum.mjs"; -import Fletcher64Checksum from "./Fletcher64Checksum.mjs"; -import Adler32Checksum from "./Adler32Checksum.mjs"; -import CRCChecksum from "./CRCChecksum.mjs"; import BLAKE2b from "./BLAKE2b.mjs"; import BLAKE2s from "./BLAKE2s.mjs"; import Streebog from "./Streebog.mjs"; @@ -112,16 +106,6 @@ class GenerateAllHashes extends Operation { {name: "SSDEEP", algo: (new SSDEEP()), inputType: "str"}, {name: "CTPH", algo: (new CTPH()), inputType: "str"} ]; - this.checksums = [ - {name: "Fletcher-8", algo: (new Fletcher8Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-16", algo: (new Fletcher16Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-32", algo: (new Fletcher32Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-64", algo: (new Fletcher64Checksum), inputType: "byteArray", params: []}, - {name: "Adler-32", algo: (new Adler32Checksum), inputType: "byteArray", params: []}, - {name: "CRC-8", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-8"]}, - {name: "CRC-16", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-16"]}, - {name: "CRC-32", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-32"]} - ]; } /** @@ -142,14 +126,6 @@ class GenerateAllHashes extends Operation { output += this.formatDigest(digest, length, includeNames, hash.name); }); - if (length === "All") { - output += "\nChecksums:\n"; - this.checksums.forEach(checksum => { - digest = this.executeAlgo(checksum.algo, checksum.inputType, checksum.params || []); - output += this.formatDigest(digest, length, includeNames, checksum.name); - }); - } - return output; } diff --git a/src/core/operations/Jsonata.mjs b/src/core/operations/Jsonata.mjs new file mode 100644 index 00000000..82cc4d39 --- /dev/null +++ b/src/core/operations/Jsonata.mjs @@ -0,0 +1,65 @@ +/** + * @author Jon K (jon@ajarsoftware.com) + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import jsonata from "jsonata"; +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Jsonata Query operation + */ +class JsonataQuery extends Operation { + /** + * JsonataQuery constructor + */ + constructor() { + super(); + + this.name = "Jsonata Query"; + this.module = "Code"; + this.description = + "Query and transform JSON data with a jsonata query."; + this.infoURL = "https://docs.jsonata.org/overview.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Query", + type: "text", + value: "string", + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + async run(input, args) { + const [query] = args; + let result, jsonObj; + + try { + jsonObj = JSON.parse(input); + } catch (err) { + throw new OperationError(`Invalid input JSON: ${err.message}`); + } + + try { + const expression = jsonata(query); + result = await expression.evaluate(jsonObj); + } catch (err) { + throw new OperationError( + `Invalid Jsonata Expression: ${err.message}` + ); + } + + return JSON.stringify(result === undefined ? "" : result); + } +} + +export default JsonataQuery; diff --git a/src/core/operations/PHPSerialize.mjs b/src/core/operations/PHPSerialize.mjs new file mode 100644 index 00000000..00fb1380 --- /dev/null +++ b/src/core/operations/PHPSerialize.mjs @@ -0,0 +1,126 @@ +/** + * @author brun0ne [brunonblok@gmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * PHP Serialize operation + */ +class PHPSerialize extends Operation { + + /** + * PHPSerialize constructor + */ + constructor() { + super(); + + this.name = "PHP Serialize"; + this.module = "Default"; + this.description = "Performs PHP serialization on JSON data.

This function does not support object tags.

Since PHP doesn't distinguish dicts and arrays, this operation is not always symmetric to PHP Deserialize.

Example:
[5,"abc",true]
becomes
a:3:{i:0;i:5;i:1;s:3:"abc";i:2;b:1;}"; + this.infoURL = "https://www.phpinternalsbook.com/php5/classes_objects/serialization.html"; + this.inputType = "JSON"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {JSON} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + /** + * Determines if a number is an integer + * @param {number} value + * @returns {boolean} + */ + function isInteger(value) { + return typeof value === "number" && parseInt(value.toString(), 10) === value; + } + + /** + * Serialize basic types + * @param {string | number | boolean} content + * @returns {string} + */ + function serializeBasicTypes(content) { + const basicTypes = { + "string": "s", + "integer": "i", + "float": "d", + "boolean": "b" + }; + /** + * Booleans + * cast to 0 or 1 + */ + if (typeof content === "boolean") { + return `${basicTypes.boolean}:${content ? 1 : 0}`; + } + /* Numbers */ + if (typeof content === "number") { + if (isInteger(content)) { + return `${basicTypes.integer}:${content.toString()}`; + } else { + return `${basicTypes.float}:${content.toString()}`; + } + } + /* Strings */ + if (typeof content === "string") + return `${basicTypes.string}:${content.length}:"${content}"`; + + /** This should be unreachable */ + throw new OperationError(`Encountered a non-implemented type: ${typeof content}`); + } + + /** + * Recursively serialize + * @param {*} object + * @returns {string} + */ + function serialize(object) { + /* Null */ + if (object == null) { + return `N;`; + } + + if (typeof object !== "object") { + /* Basic types */ + return `${serializeBasicTypes(object)};`; + } else if (object instanceof Array) { + /* Arrays */ + const serializedElements = []; + + for (let i = 0; i < object.length; i++) { + serializedElements.push(`${serialize(i)}${serialize(object[i])}`); + } + + return `a:${object.length}:{${serializedElements.join("")}}`; + } else if (object instanceof Object) { + /** + * Objects + * Note: the output cannot be guaranteed to be in the same order as the input + */ + const serializedElements = []; + const keys = Object.keys(object); + + for (const key of keys) { + serializedElements.push(`${serialize(key)}${serialize(object[key])}`); + } + + return `a:${keys.length}:{${serializedElements.join("")}}`; + } + + /** This should be unreachable */ + throw new OperationError(`Encountered a non-implemented type: ${typeof object}`); + } + + return serialize(input); + } +} + +export default PHPSerialize; diff --git a/src/core/operations/RailFenceCipherDecode.mjs b/src/core/operations/RailFenceCipherDecode.mjs index be54ee12..39795f21 100644 --- a/src/core/operations/RailFenceCipherDecode.mjs +++ b/src/core/operations/RailFenceCipherDecode.mjs @@ -72,7 +72,7 @@ class RailFenceCipherDecode extends Operation { } } - return plaintext.join("").trim(); + return plaintext.join(""); } } diff --git a/src/core/operations/RailFenceCipherEncode.mjs b/src/core/operations/RailFenceCipherEncode.mjs index 03651f85..89eddde7 100644 --- a/src/core/operations/RailFenceCipherEncode.mjs +++ b/src/core/operations/RailFenceCipherEncode.mjs @@ -66,7 +66,7 @@ class RailFenceCipherEncode extends Operation { rows[rowIdx] += plaintext[pos]; } - return rows.join("").trim(); + return rows.join(""); } } diff --git a/src/core/operations/ToBase32.mjs b/src/core/operations/ToBase32.mjs index fd36f550..44eb8b48 100644 --- a/src/core/operations/ToBase32.mjs +++ b/src/core/operations/ToBase32.mjs @@ -6,6 +6,7 @@ import Operation from "../Operation.mjs"; import Utils from "../Utils.mjs"; +import {ALPHABET_OPTIONS} from "../lib/Base32.mjs"; /** * To Base32 operation @@ -27,8 +28,8 @@ class ToBase32 extends Operation { this.args = [ { name: "Alphabet", - type: "binaryString", - value: "A-Z2-7=" + type: "editableOption", + value: ALPHABET_OPTIONS } ]; } @@ -83,3 +84,4 @@ class ToBase32 extends Operation { } export default ToBase32; + diff --git a/src/core/operations/URLDecode.mjs b/src/core/operations/URLDecode.mjs index 7d6544ac..bb6c0612 100644 --- a/src/core/operations/URLDecode.mjs +++ b/src/core/operations/URLDecode.mjs @@ -23,7 +23,13 @@ class URLDecode extends Operation { this.infoURL = "https://wikipedia.org/wiki/Percent-encoding"; this.inputType = "string"; this.outputType = "string"; - this.args = []; + this.args = [ + { + "name": "Treat \"+\" as space", + "type": "boolean", + "value": true + }, + ]; this.checks = [ { pattern: ".*(?:%[\\da-f]{2}.*){4}", @@ -39,7 +45,8 @@ class URLDecode extends Operation { * @returns {string} */ run(input, args) { - const data = input.replace(/\+/g, "%20"); + const plusIsSpace = args[0]; + const data = plusIsSpace ? input.replace(/\+/g, "%20") : input; try { return decodeURIComponent(data); } catch (err) { diff --git a/src/core/operations/XORChecksum.mjs b/src/core/operations/XORChecksum.mjs new file mode 100644 index 00000000..1603a265 --- /dev/null +++ b/src/core/operations/XORChecksum.mjs @@ -0,0 +1,59 @@ +/** + * @author Thomas Weißschuh [thomas@t-8ch.de] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import { toHex } from "../lib/Hex.mjs"; + +/** + * XOR Checksum operation + */ +class XORChecksum extends Operation { + + /** + * XORChecksum constructor + */ + constructor() { + super(); + + this.name = "XOR Checksum"; + this.module = "Crypto"; + this.description = "XOR Checksum splits the input into blocks of a configurable size and performs the XOR operation on these blocks."; + this.infoURL = "https://wikipedia.org/wiki/XOR"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + name: "Blocksize", + type: "number", + value: 4 + }, + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const blocksize = args[0]; + input = new Uint8Array(input); + + const res = Array(blocksize); + res.fill(0); + + for (const chunk of Utils.chunked(input, blocksize)) { + for (let i = 0; i < blocksize; i++) { + res[i] ^= chunk[i]; + } + } + + return toHex(res, ""); + } +} + +export default XORChecksum; diff --git a/src/node/api.mjs b/src/node/api.mjs index 0c9dd8a7..88b3f834 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -74,11 +74,11 @@ function transformArgs(opArgsList, newArgs) { return opArgs.map((arg) => { if (arg.type === "option") { // pick default option if not already chosen - return typeof arg.value === "string" ? arg.value : arg.value[0]; + return typeof arg.value === "string" ? arg.value : arg.value[arg.defaultIndex ?? 0]; } if (arg.type === "editableOption") { - return typeof arg.value === "string" ? arg.value : arg.value[0].value; + return typeof arg.value === "string" ? arg.value : arg.value[arg.defaultIndex ?? 0].value; } if (arg.type === "toggleString") { diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 2e87210c..ae972a59 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -125,6 +125,7 @@ class Manager { window.addEventListener("focus", this.window.windowFocus.bind(this.window)); window.addEventListener("statechange", this.app.stateChange.bind(this.app)); window.addEventListener("popstate", this.app.popState.bind(this.app)); + window.addEventListener("message", this.input.handlePostMessage.bind(this.input)); // Controls document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls)); diff --git a/src/web/static/structuredData.json b/src/web/static/structuredData.json index 08677281..e242f70e 100755 --- a/src/web/static/structuredData.json +++ b/src/web/static/structuredData.json @@ -1,23 +1,26 @@ [ { "@context": "http://schema.org", - "@type": "Organization", - "url": "https://gchq.github.io/CyberChef/", - "logo": "https://gchq.github.io/CyberChef/images/cyberchef-128x128.png", - "sameAs": [ - "https://github.com/gchq/CyberChef", - "https://www.npmjs.com/package/cyberchef" + "@graph": [ + { + "@type": "Organization", + "url": "https://gchq.github.io/CyberChef/", + "logo": "https://gchq.github.io/CyberChef/images/cyberchef-128x128.png", + "sameAs": [ + "https://github.com/gchq/CyberChef", + "https://www.npmjs.com/package/cyberchef" + ] + }, + { + "@type": "WebSite", + "url": "https://gchq.github.io/CyberChef/", + "name": "CyberChef", + "potentialAction": { + "@type": "SearchAction", + "target": "https://gchq.github.io/CyberChef/?op={operation_search_term}", + "query-input": "required name=operation_search_term" + } + } ] - }, - { - "@context": "http://schema.org", - "@type": "WebSite", - "url": "https://gchq.github.io/CyberChef/", - "name": "CyberChef", - "potentialAction": { - "@type": "SearchAction", - "target": "https://gchq.github.io/CyberChef/?op={operation_search_term}", - "query-input": "required name=operation_search_term" - } } -] +] \ No newline at end of file diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index 3999fd2f..d32ed9d1 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -1654,6 +1654,23 @@ class InputWaiter { this.changeTab(inputNum, this.app.options.syncTabs); } + /** + * Handler for incoming postMessages + * If the events data has a `type` property set to `dataSubmit` + * the value property is set to the current input + * @param {event} e + * @param {object} e.data + * @param {string} e.data.type - the type of request, currently the only value is "dataSubmit" + * @param {string} e.data.value - the value of the message + */ + handlePostMessage(e) { + log.debug(e); + if ("data" in e && "id" in e.data && "value" in e.data) { + if (e.data.id === "setInput") { + this.setInput(e.data.value); + } + } + } } export default InputWaiter; diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index 3f5aa302..93ca1182 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -8,6 +8,7 @@ import HTMLOperation from "../HTMLOperation.mjs"; import Sortable from "sortablejs"; import Utils from "../../core/Utils.mjs"; import {escapeControlChars} from "../utils/editorUtils.mjs"; +import DOMPurify from "dompurify"; /** @@ -435,7 +436,9 @@ class RecipeWaiter { const item = document.createElement("li"); item.classList.add("operation"); - item.innerHTML = name; + const clean = DOMPurify.sanitize(name); + item.innerHTML = clean; + this.buildRecipeOperation(item); document.getElementById("rec-list").appendChild(item); diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 12d31870..06eaf86f 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -11,15 +11,14 @@ * @license Apache-2.0 */ -import { - setLongTestFailure, - logTestReport, -} from "../lib/utils.mjs"; +import { setLongTestFailure, logTestReport } from "../lib/utils.mjs"; import TestRegister from "../lib/TestRegister.mjs"; import "./tests/AESKeyWrap.mjs"; +import "./tests/AlternatingCaps.mjs"; import "./tests/AvroToJSON.mjs"; import "./tests/BaconCipher.mjs"; +import "./tests/Base32.mjs"; import "./tests/Base45.mjs"; import "./tests/Base58.mjs"; import "./tests/Base62.mjs"; @@ -30,6 +29,7 @@ import "./tests/BCD.mjs"; import "./tests/BitwiseOp.mjs"; import "./tests/BLAKE2b.mjs"; import "./tests/BLAKE2s.mjs"; +import "./tests/BLAKE3.mjs"; import "./tests/Bombe.mjs"; import "./tests/BSON.mjs"; import "./tests/ByteRepr.mjs"; @@ -66,11 +66,13 @@ import "./tests/ELFInfo.mjs"; import "./tests/Enigma.mjs"; import "./tests/ExtractEmailAddresses.mjs"; import "./tests/ExtractHashes.mjs"; +import "./tests/ExtractIPAddresses.mjs"; import "./tests/Float.mjs"; import "./tests/FileTree.mjs"; import "./tests/FletcherChecksum.mjs"; import "./tests/Fork.mjs"; import "./tests/FromDecimal.mjs"; +import "./tests/GenerateAllChecksums.mjs"; import "./tests/GenerateAllHashes.mjs"; import "./tests/GenerateDeBruijnSequence.mjs"; import "./tests/GetAllCasings.mjs"; @@ -88,6 +90,7 @@ import "./tests/IndexOfCoincidence.mjs"; import "./tests/JA3Fingerprint.mjs"; import "./tests/JA4.mjs"; import "./tests/JA3SFingerprint.mjs"; +import "./tests/Jsonata.mjs"; import "./tests/JSONBeautify.mjs"; import "./tests/JSONMinify.mjs"; import "./tests/JSONtoCSV.mjs"; @@ -125,6 +128,7 @@ import "./tests/ParseUDP.mjs"; import "./tests/PEMtoHex.mjs"; import "./tests/PGP.mjs"; import "./tests/PHP.mjs"; +import "./tests/PHPSerialize.mjs"; import "./tests/PowerSet.mjs"; import "./tests/Protobuf.mjs"; import "./tests/PubKeyFromCert.mjs"; @@ -161,6 +165,7 @@ import "./tests/TranslateDateTimeFormat.mjs"; import "./tests/Typex.mjs"; import "./tests/UnescapeString.mjs"; import "./tests/Unicode.mjs"; +import "./tests/URLEncodeDecode.mjs"; import "./tests/RSA.mjs"; import "./tests/CBOREncode.mjs"; import "./tests/CBORDecode.mjs"; @@ -179,14 +184,14 @@ const testStatus = { allTestsPassing: true, counts: { total: 0, - } + }, }; setLongTestFailure(); const logOpsTestReport = logTestReport.bind(null, testStatus); -(async function() { +(async function () { const results = await TestRegister.runTests(); logOpsTestReport(results); })(); diff --git a/tests/operations/tests/AlternatingCaps.mjs b/tests/operations/tests/AlternatingCaps.mjs new file mode 100644 index 00000000..b23768eb --- /dev/null +++ b/tests/operations/tests/AlternatingCaps.mjs @@ -0,0 +1,19 @@ +/* @author sw5678 + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + "name": "AlternatingCaps: Basic Example", + "input": "Hello, world!", + "expectedOutput": "hElLo, WoRlD!", + "recipeConfig": [ + { + "op": "Alternating Caps", + "args": [] + }, + ], + } +]); diff --git a/tests/operations/tests/BLAKE3.mjs b/tests/operations/tests/BLAKE3.mjs new file mode 100644 index 00000000..42cb4dc1 --- /dev/null +++ b/tests/operations/tests/BLAKE3.mjs @@ -0,0 +1,55 @@ +/** + * BLAKE3 tests. + * @author xumptex [xumptex@outlook.fr] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "BLAKE3: 8 - Hello world", + input: "Hello world", + expectedOutput: "e7e6fb7d2869d109", + recipeConfig: [ + { "op": "BLAKE3", + "args": [8, ""] } + ] + }, + { + name: "BLAKE3: 16 - Hello world 2", + input: "Hello world 2", + expectedOutput: "2a3df5fe5f0d3fcdd995fc203c7f7c52", + recipeConfig: [ + { "op": "BLAKE3", + "args": [16, ""] } + ] + }, + { + name: "BLAKE3: 32 - Hello world", + input: "Hello world", + expectedOutput: "e7e6fb7d2869d109b62cdb1227208d4016cdaa0af6603d95223c6a698137d945", + recipeConfig: [ + { "op": "BLAKE3", + "args": [32, ""] } + ] + }, + { + name: "BLAKE3: Key Test", + input: "Hello world", + expectedOutput: "59dd23ac9d025690", + recipeConfig: [ + { "op": "BLAKE3", + "args": [8, "ThiskeyisexactlythirtytwoBytesLo"] } + ] + }, + { + name: "BLAKE3: Key Test 2", + input: "Hello world", + expectedOutput: "c8302c9634c1da42", + recipeConfig: [ + { "op": "BLAKE3", + "args": [8, "ThiskeyisexactlythirtytwoByteslo"] } + ] + } +]); diff --git a/tests/operations/tests/Base32.mjs b/tests/operations/tests/Base32.mjs new file mode 100644 index 00000000..760cdf14 --- /dev/null +++ b/tests/operations/tests/Base32.mjs @@ -0,0 +1,176 @@ +/** + * Base32 Tests + * + * @author Peter C-S [petercs@purelymail.com] + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; +import {ALPHABET_OPTIONS} from "../../../src/core/lib/Base32.mjs"; + +// Example Standard Base32 Tests +const STANDARD_INP = "HELLO BASE32"; +const STANDARD_OUT = "JBCUYTCPEBBECU2FGMZA===="; + +// Example Hex Extended Base32 Tests +const EXTENDED_INP = "HELLO BASE32 EXTENDED"; +const EXTENDED_OUT = "912KOJ2F41142KQ56CP20HAOAH2KSH258G======"; + +// All Bytes +const ALL_BYTES = [ + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", + "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f", + "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f", + "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f", + "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f", + "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f", + "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f", + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f", + "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf", + "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf", + "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf", + "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef", + "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", +].join(""); + +const ALL_BYTES_EXTENDED_OUT = "000G40O40K30E209185GO38E1S8124GJ2GAHC5OO34D1M70T3OFI08924CI2A9H750KIKAPC5KN2UC1H68PJ8D9M6SS3IEHR7GUJSFQ085146H258P3KGIAA9D64QJIFA18L4KQKALB5EM2PB9DLONAUBTG62OJ3CHIMCPR8D5L6MR3DDPNN0SBIEDQ7ATJNF1SNKURSFLV7V041GA1O91C6GU48J2KBHI6OT3SGI699754LIQBPH6CQJEE9R7KVK2GQ58T4KMJAFA59LALQPBDELUOB3CLJMIQRDDTON6TBNF5TNQVS1GE2OF2CBHM7P34SLIUCPN7CVK6HQB9T9LEMQVCDJMMRRJETTNV0S7HE7P75SRJUHQFATFMERRNFU3OV5SVKUNRFFU7PVBTVPVFUVS======"; +const ALL_BYTES_STANDARD_OUT = "AAAQEAYEAUDAOCAJBIFQYDIOB4IBCEQTCQKRMFYYDENBWHA5DYPSAIJCEMSCKJRHFAUSUKZMFUXC6MBRGIZTINJWG44DSOR3HQ6T4P2AIFBEGRCFIZDUQSKKJNGE2TSPKBIVEU2UKVLFOWCZLJNVYXK6L5QGCYTDMRSWMZ3INFVGW3DNNZXXA4LSON2HK5TXPB4XU634PV7H7AEBQKBYJBMGQ6EITCULRSGY5D4QSGJJHFEVS2LZRGM2TOOJ3HU7UCQ2FI5EUWTKPKFJVKV2ZLNOV6YLDMVTWS23NN5YXG5LXPF5X274BQOCYPCMLRWHZDE4VS6MZXHM7UGR2LJ5JVOW27MNTWW33TO55X7A4HROHZHF43T6R2PK5PWO33XP6DY7F47U6X3PP6HZ7L57Z7P674======"; + +TestRegister.addTests([ + { + name: "To Base32 Standard: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[0].value], + }, + ], + }, + { + name: "To Base32 Hex Extended: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[1].value], + }, + ], + }, + { + name: "From Base32 Standard: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[0].value, false], + }, + ], + }, + { + name: "From Base32 Hex Extended: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[1].value, false], + }, + ], + }, + { + name: "To Base32 Standard: " + STANDARD_INP, + input: STANDARD_INP, + expectedOutput: STANDARD_OUT, + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[0].value], + }, + ], + }, + { + name: "To Base32 Hex Extended: " + EXTENDED_INP, + input: EXTENDED_INP, + expectedOutput: EXTENDED_OUT, + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[1].value], + }, + ], + }, + { + name: "From Base32 Standard: " + STANDARD_OUT, + input: STANDARD_OUT, + expectedOutput: STANDARD_INP, + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[0].value, false], + }, + ], + }, + { + name: "From Base32 Hex Extended: " + EXTENDED_OUT, + input: EXTENDED_OUT, + expectedOutput: EXTENDED_INP, + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[1].value, false], + }, + ], + }, + { + name: "To Base32 Hex Standard: All Bytes", + input: ALL_BYTES, + expectedOutput: ALL_BYTES_STANDARD_OUT, + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[0].value], + }, + ], + }, + { + name: "To Base32 Hex Extended: All Bytes", + input: ALL_BYTES, + expectedOutput: ALL_BYTES_EXTENDED_OUT, + recipeConfig: [ + { + op: "To Base32", + args: [ALPHABET_OPTIONS[1].value], + }, + ], + }, + { + name: "From Base32 Hex Standard: All Bytes", + input: ALL_BYTES_STANDARD_OUT, + expectedOutput: ALL_BYTES, + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[0].value, false], + }, + ], + }, + { + name: "From Base32 Hex Extended: All Bytes", + input: ALL_BYTES_EXTENDED_OUT, + expectedOutput: ALL_BYTES, + recipeConfig: [ + { + op: "From Base32", + args: [ALPHABET_OPTIONS[1].value, false], + }, + ], + }, +]); + diff --git a/tests/operations/tests/Ciphers.mjs b/tests/operations/tests/Ciphers.mjs index 47453cf7..16aba950 100644 --- a/tests/operations/tests/Ciphers.mjs +++ b/tests/operations/tests/Ciphers.mjs @@ -528,4 +528,15 @@ TestRegister.addTests([ } ], }, + { + name: "Rail Fence Cipher Encode: Normal with Offset with Spaces", + input: "No one expects the spanish Inquisition.", + expectedOutput: " e n ut.ooeepcstesaihIqiiinNnxthpsnso", + recipeConfig: [ + { + "op": "Rail Fence Cipher Encode", + "args": [3, 2] + } + ], + }, ]); diff --git a/tests/operations/tests/ECDSA.mjs b/tests/operations/tests/ECDSA.mjs index 560afc5c..ffffbb10 100644 --- a/tests/operations/tests/ECDSA.mjs +++ b/tests/operations/tests/ECDSA.mjs @@ -6,7 +6,10 @@ * @license Apache-2.0 */ import TestRegister from "../../lib/TestRegister.mjs"; -import { ASCII_TEXT } from "../../samples/Ciphers.mjs"; +import {ALL_BYTES, ASCII_TEXT, UTF8_TEXT} from "../../samples/Ciphers.mjs"; + +const SOME_HEX_BYTES = "cdb23f958e018418621d9e489b7bba0f0c481f604eba2eb1ea35e38f99490cc0"; +const SOME_BASE64_BYTES = "zbI/lY4BhBhiHZ5Im3u6DwxIH2BOui6x6jXjj5lJDMA="; const P256 = { // openssl ecparam -name prime256v1 -genkey -noout -out p256.priv.key @@ -104,7 +107,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "MD5", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "MD5", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -119,7 +122,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-1", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-1", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -134,7 +137,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -149,7 +152,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-384", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-384", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -164,7 +167,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-512", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-512", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -179,7 +182,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -194,7 +197,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-384", P384.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-384", P384.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -209,7 +212,7 @@ TestRegister.addTests([ }, { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-512", P521.publicKey, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-512", P521.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -246,7 +249,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -257,7 +260,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -268,7 +271,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -279,7 +282,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -290,7 +293,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -301,7 +304,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT, "Raw"] } ] }, @@ -312,7 +315,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-256", P256.privateKeyPkcs1, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-256", P256.privateKeyPkcs1, ASCII_TEXT, "Raw"] } ] }, @@ -323,7 +326,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Verify", - "args": ["ASN.1 HEX", "SHA-256", PEM_PUB_RSA512, ASCII_TEXT] + "args": ["ASN.1 HEX", "SHA-256", PEM_PUB_RSA512, ASCII_TEXT, "Raw"] } ] }, @@ -460,5 +463,73 @@ TestRegister.addTests([ "args": ["Auto", "Raw JSON"] } ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA256 UTF8", + input: UTF8_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, UTF8_TEXT, "Raw"] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA256 bytes raw", + input: ALL_BYTES, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ALL_BYTES, "Raw"] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA256 bytes hex", + input: SOME_HEX_BYTES, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, SOME_HEX_BYTES, "Hex"] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA256 bytes Base64", + input: SOME_BASE64_BYTES, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "From Base64", + "args": ["A-Za-z0-9+/=", true] + }, + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, SOME_BASE64_BYTES, "Base64"] + } + ] } ]); diff --git a/tests/operations/tests/ExtractIPAddresses.mjs b/tests/operations/tests/ExtractIPAddresses.mjs new file mode 100644 index 00000000..13922e64 --- /dev/null +++ b/tests/operations/tests/ExtractIPAddresses.mjs @@ -0,0 +1,133 @@ +/** + * ExtractIPAddresses tests. + * + * @author gchqdev365 [gchqdev365@outlook.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "ExtractIPAddress All Zeros", + input: "0.0.0.0", + expectedOutput: "0.0.0.0", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress All 10s", + input: "10.10.10.10", + expectedOutput: "10.10.10.10", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress All 10s", + input: "100.100.100.100", + expectedOutput: "100.100.100.100", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress 255s", + input: "255.255.255.255", + expectedOutput: "255.255.255.255", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress double digits", + input: "10.10.10.10 25.25.25.25 99.99.99.99", + expectedOutput: "10.10.10.10\n25.25.25.25\n99.99.99.99", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress 256 in middle", + input: "255.256.255.255 255.255.256.255", + expectedOutput: "", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress 256 at each end", + input: "256.255.255.255 255.255.255.256", + expectedOutput: "", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress silly example", + input: "710.65.0.456", + expectedOutput: "", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress longer dotted decimal", + input: "1.2.3.4.5.6.7.8", + expectedOutput: "1.2.3.4\n5.6.7.8", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress octal valid", + input: "01.01.01.01 0123.0123.0123.0123 0377.0377.0377.0377", + expectedOutput: "01.01.01.01\n0123.0123.0123.0123\n0377.0377.0377.0377", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, + { + name: "ExtractIPAddress octal invalid", + input: "0378.01.01.01 03.0377.2.3", + expectedOutput: "", + recipeConfig: [ + { + "op": "Extract IP addresses", + "args": [true, true, false, false, false, false] + }, + ], + }, +]); + diff --git a/tests/operations/tests/GenerateAllChecksums.mjs b/tests/operations/tests/GenerateAllChecksums.mjs new file mode 100644 index 00000000..46c4e38c --- /dev/null +++ b/tests/operations/tests/GenerateAllChecksums.mjs @@ -0,0 +1,805 @@ +/** + * GenerateAllChecksums tests. + * + * @author r4mos [2k95ljkhg@mozmail.com] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +const CHECK_STRING = "123456789"; + +TestRegister.addTests([ + { + name: "Full generate all checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-3/GSM: 4 +CRC-3/ROHC: 6 +CRC-4/G-704: 7 +CRC-4/INTERLAKEN: b +CRC-4/ITU: 7 +CRC-5/EPC: 00 +CRC-5/EPC-C1G2: 00 +CRC-5/G-704: 07 +CRC-5/ITU: 07 +CRC-5/USB: 19 +CRC-6/CDMA2000-A: 0d +CRC-6/CDMA2000-B: 3b +CRC-6/DARC: 26 +CRC-6/G-704: 06 +CRC-6/GSM: 13 +CRC-6/ITU: 06 +CRC-7/MMC: 75 +CRC-7/ROHC: 53 +CRC-7/UMTS: 61 +CRC-8: f4 +CRC-8/8H2F: df +CRC-8/AES: 97 +CRC-8/AUTOSAR: df +CRC-8/BLUETOOTH: 26 +CRC-8/CDMA2000: da +CRC-8/DARC: 15 +CRC-8/DVB-S2: bc +CRC-8/EBU: 97 +CRC-8/GSM-A: 37 +CRC-8/GSM-B: 94 +CRC-8/HITAG: b4 +CRC-8/I-432-1: a1 +CRC-8/I-CODE: 7e +CRC-8/ITU: a1 +CRC-8/LTE: ea +CRC-8/MAXIM: a1 +CRC-8/MAXIM-DOW: a1 +CRC-8/MIFARE-MAD: 99 +CRC-8/NRSC-5: f7 +CRC-8/OPENSAFETY: 3e +CRC-8/ROHC: d0 +CRC-8/SAE-J1850: 4b +CRC-8/SAE-J1850-ZERO: 37 +CRC-8/SMBUS: f4 +CRC-8/TECH-3250: 97 +CRC-8/WCDMA: 25 +Fletcher-8: 0c +CRC-10/ATM: 199 +CRC-10/CDMA2000: 233 +CRC-10/GSM: 12a +CRC-10/I-610: 199 +CRC-11/FLEXRAY: 5a3 +CRC-11/UMTS: 061 +CRC-12/3GPP: daf +CRC-12/CDMA2000: d4d +CRC-12/DECT: f5b +CRC-12/GSM: b34 +CRC-12/UMTS: daf +CRC-13/BBC: 04fa +CRC-14/DARC: 082d +CRC-14/GSM: 30ae +CRC-15/CAN: 059e +CRC-15/MPT1327: 2566 +CRC-16: bb3d +CRC-16/A: bf05 +CRC-16/ACORN: 31c3 +CRC-16/ARC: bb3d +CRC-16/AUG-CCITT: e5cc +CRC-16/AUTOSAR: 29b1 +CRC-16/B: 906e +CRC-16/BLUETOOTH: 2189 +CRC-16/BUYPASS: fee8 +CRC-16/CCITT: 2189 +CRC-16/CCITT-FALSE: 29b1 +CRC-16/CCITT-TRUE: 2189 +CRC-16/CCITT-ZERO: 31c3 +CRC-16/CDMA2000: 4c06 +CRC-16/CMS: aee7 +CRC-16/DARC: d64e +CRC-16/DDS-110: 9ecf +CRC-16/DECT-R: 007e +CRC-16/DECT-X: 007f +CRC-16/DNP: ea82 +CRC-16/EN-13757: c2b7 +CRC-16/EPC: d64e +CRC-16/EPC-C1G2: d64e +CRC-16/GENIBUS: d64e +CRC-16/GSM: ce3c +CRC-16/I-CODE: d64e +CRC-16/IBM: bb3d +CRC-16/IBM-3740: 29b1 +CRC-16/IBM-SDLC: 906e +CRC-16/IEC-61158-2: a819 +CRC-16/ISO-HDLC: 906e +CRC-16/ISO-IEC-14443-3-A: bf05 +CRC-16/ISO-IEC-14443-3-B: 906e +CRC-16/KERMIT: 2189 +CRC-16/LHA: bb3d +CRC-16/LJ1200: bdf4 +CRC-16/LTE: 31c3 +CRC-16/M17: 772b +CRC-16/MAXIM: 44c2 +CRC-16/MAXIM-DOW: 44c2 +CRC-16/MCRF4XX: 6f91 +CRC-16/MODBUS: 4b37 +CRC-16/NRSC-5: a066 +CRC-16/OPENSAFETY-A: 5d38 +CRC-16/OPENSAFETY-B: 20fe +CRC-16/PROFIBUS: a819 +CRC-16/RIELLO: 63d0 +CRC-16/SPI-FUJITSU: e5cc +CRC-16/T10-DIF: d0db +CRC-16/TELEDISK: 0fb3 +CRC-16/TMS37157: 26b1 +CRC-16/UMTS: fee8 +CRC-16/USB: b4c8 +CRC-16/V-41-LSB: 2189 +CRC-16/V-41-MSB: 31c3 +CRC-16/VERIFONE: fee8 +CRC-16/X-25: 906e +CRC-16/XMODEM: 31c3 +CRC-16/ZMODEM: 31c3 +Fletcher-16: 1ede +CRC-17/CAN-FD: 04f03 +CRC-21/CAN-FD: 0ed841 +CRC-24/BLE: c25a56 +CRC-24/FLEXRAY-A: 7979bd +CRC-24/FLEXRAY-B: 1f23b8 +CRC-24/INTERLAKEN: b4f3e6 +CRC-24/LTE-A: cde703 +CRC-24/LTE-B: 23ef52 +CRC-24/OPENPGP: 21cf02 +CRC-24/OS-9: 200fa5 +CRC-30/CDMA: 04c34abf +CRC-31/PHILIPS: 0ce9e46c +Adler-32: 091e01de +CRC-32: cbf43926 +CRC-32/AAL5: fc891918 +CRC-32/ADCCP: cbf43926 +CRC-32/AIXM: 3010bf7f +CRC-32/AUTOSAR: 1697d06a +CRC-32/BASE91-C: e3069283 +CRC-32/BASE91-D: 87315576 +CRC-32/BZIP2: fc891918 +CRC-32/C: e3069283 +CRC-32/CASTAGNOLI: e3069283 +CRC-32/CD-ROM-EDC: 6ec2edc4 +CRC-32/CKSUM: 765e7680 +CRC-32/D: 87315576 +CRC-32/DECT-B: fc891918 +CRC-32/INTERLAKEN: e3069283 +CRC-32/ISCSI: e3069283 +CRC-32/ISO-HDLC: cbf43926 +CRC-32/JAMCRC: 340bc6d9 +CRC-32/MEF: d2c22f51 +CRC-32/MPEG-2: 0376e6e7 +CRC-32/NVME: e3069283 +CRC-32/PKZIP: cbf43926 +CRC-32/POSIX: 765e7680 +CRC-32/Q: 3010bf7f +CRC-32/SATA: cf72afe8 +CRC-32/V-42: cbf43926 +CRC-32/XFER: bd0be338 +CRC-32/XZ: cbf43926 +Fletcher-32: df09d509 +CRC-40/GSM: d4164fc646 +CRC-64/ECMA-182: 6c40df5f0b497347 +CRC-64/GO-ECMA: 995dc9bbdf1939fa +CRC-64/GO-ISO: b90956c775a41001 +CRC-64/MS: 75d4b74f024eceea +CRC-64/NVME: ae8b14860a799888 +CRC-64/REDIS: e9c6d914c4b8d9ca +CRC-64/WE: 62ec59e3f1a4f00a +CRC-64/XZ: 995dc9bbdf1939fa +Fletcher-64: 0d0803376c6a689f +CRC-82/DARC: 09ea83f625023801fd612 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["All", true] + } + ] + }, + { + name: "Full generate all checksums without name", + input: CHECK_STRING, + expectedOutput: `4 +6 +7 +b +7 +00 +00 +07 +07 +19 +0d +3b +26 +06 +13 +06 +75 +53 +61 +f4 +df +97 +df +26 +da +15 +bc +97 +37 +94 +b4 +a1 +7e +a1 +ea +a1 +a1 +99 +f7 +3e +d0 +4b +37 +f4 +97 +25 +0c +199 +233 +12a +199 +5a3 +061 +daf +d4d +f5b +b34 +daf +04fa +082d +30ae +059e +2566 +bb3d +bf05 +31c3 +bb3d +e5cc +29b1 +906e +2189 +fee8 +2189 +29b1 +2189 +31c3 +4c06 +aee7 +d64e +9ecf +007e +007f +ea82 +c2b7 +d64e +d64e +d64e +ce3c +d64e +bb3d +29b1 +906e +a819 +906e +bf05 +906e +2189 +bb3d +bdf4 +31c3 +772b +44c2 +44c2 +6f91 +4b37 +a066 +5d38 +20fe +a819 +63d0 +e5cc +d0db +0fb3 +26b1 +fee8 +b4c8 +2189 +31c3 +fee8 +906e +31c3 +31c3 +1ede +04f03 +0ed841 +c25a56 +7979bd +1f23b8 +b4f3e6 +cde703 +23ef52 +21cf02 +200fa5 +04c34abf +0ce9e46c +091e01de +cbf43926 +fc891918 +cbf43926 +3010bf7f +1697d06a +e3069283 +87315576 +fc891918 +e3069283 +e3069283 +6ec2edc4 +765e7680 +87315576 +fc891918 +e3069283 +e3069283 +cbf43926 +340bc6d9 +d2c22f51 +0376e6e7 +e3069283 +cbf43926 +765e7680 +3010bf7f +cf72afe8 +cbf43926 +bd0be338 +cbf43926 +df09d509 +d4164fc646 +6c40df5f0b497347 +995dc9bbdf1939fa +b90956c775a41001 +75d4b74f024eceea +ae8b14860a799888 +e9c6d914c4b8d9ca +62ec59e3f1a4f00a +995dc9bbdf1939fa +0d0803376c6a689f +09ea83f625023801fd612 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["All", false] + } + ] + }, + { + name: "Full generate 3 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-3/GSM: 4 +CRC-3/ROHC: 6 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["3", true] + } + ] + }, + { + name: "Full generate 4 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-4/G-704: 7 +CRC-4/INTERLAKEN: b +CRC-4/ITU: 7 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["4", true] + } + ] + }, + { + name: "Full generate 5 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-5/EPC: 00 +CRC-5/EPC-C1G2: 00 +CRC-5/G-704: 07 +CRC-5/ITU: 07 +CRC-5/USB: 19 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["5", true] + } + ] + }, + { + name: "Full generate 6 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-6/CDMA2000-A: 0d +CRC-6/CDMA2000-B: 3b +CRC-6/DARC: 26 +CRC-6/G-704: 06 +CRC-6/GSM: 13 +CRC-6/ITU: 06 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["6", true] + } + ] + }, + { + name: "Full generate 7 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-7/MMC: 75 +CRC-7/ROHC: 53 +CRC-7/UMTS: 61 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["7", true] + } + ] + }, + { + name: "Full generate 8 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-8: f4 +CRC-8/8H2F: df +CRC-8/AES: 97 +CRC-8/AUTOSAR: df +CRC-8/BLUETOOTH: 26 +CRC-8/CDMA2000: da +CRC-8/DARC: 15 +CRC-8/DVB-S2: bc +CRC-8/EBU: 97 +CRC-8/GSM-A: 37 +CRC-8/GSM-B: 94 +CRC-8/HITAG: b4 +CRC-8/I-432-1: a1 +CRC-8/I-CODE: 7e +CRC-8/ITU: a1 +CRC-8/LTE: ea +CRC-8/MAXIM: a1 +CRC-8/MAXIM-DOW: a1 +CRC-8/MIFARE-MAD: 99 +CRC-8/NRSC-5: f7 +CRC-8/OPENSAFETY: 3e +CRC-8/ROHC: d0 +CRC-8/SAE-J1850: 4b +CRC-8/SAE-J1850-ZERO: 37 +CRC-8/SMBUS: f4 +CRC-8/TECH-3250: 97 +CRC-8/WCDMA: 25 +Fletcher-8: 0c +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["8", true] + } + ] + }, + { + name: "Full generate 10 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-10/ATM: 199 +CRC-10/CDMA2000: 233 +CRC-10/GSM: 12a +CRC-10/I-610: 199 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["10", true] + } + ] + }, + { + name: "Full generate 11 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-11/FLEXRAY: 5a3 +CRC-11/UMTS: 061 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["11", true] + } + ] + }, + { + name: "Full generate 12 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-12/3GPP: daf +CRC-12/CDMA2000: d4d +CRC-12/DECT: f5b +CRC-12/GSM: b34 +CRC-12/UMTS: daf +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["12", true] + } + ] + }, + { + name: "Full generate 13 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-13/BBC: 04fa +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["13", true] + } + ] + }, + { + name: "Full generate 14 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-14/DARC: 082d +CRC-14/GSM: 30ae +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["14", true] + } + ] + }, + { + name: "Full generate 15 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-15/CAN: 059e +CRC-15/MPT1327: 2566 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["15", true] + } + ] + }, + { + name: "Full generate 16 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-16: bb3d +CRC-16/A: bf05 +CRC-16/ACORN: 31c3 +CRC-16/ARC: bb3d +CRC-16/AUG-CCITT: e5cc +CRC-16/AUTOSAR: 29b1 +CRC-16/B: 906e +CRC-16/BLUETOOTH: 2189 +CRC-16/BUYPASS: fee8 +CRC-16/CCITT: 2189 +CRC-16/CCITT-FALSE: 29b1 +CRC-16/CCITT-TRUE: 2189 +CRC-16/CCITT-ZERO: 31c3 +CRC-16/CDMA2000: 4c06 +CRC-16/CMS: aee7 +CRC-16/DARC: d64e +CRC-16/DDS-110: 9ecf +CRC-16/DECT-R: 007e +CRC-16/DECT-X: 007f +CRC-16/DNP: ea82 +CRC-16/EN-13757: c2b7 +CRC-16/EPC: d64e +CRC-16/EPC-C1G2: d64e +CRC-16/GENIBUS: d64e +CRC-16/GSM: ce3c +CRC-16/I-CODE: d64e +CRC-16/IBM: bb3d +CRC-16/IBM-3740: 29b1 +CRC-16/IBM-SDLC: 906e +CRC-16/IEC-61158-2: a819 +CRC-16/ISO-HDLC: 906e +CRC-16/ISO-IEC-14443-3-A: bf05 +CRC-16/ISO-IEC-14443-3-B: 906e +CRC-16/KERMIT: 2189 +CRC-16/LHA: bb3d +CRC-16/LJ1200: bdf4 +CRC-16/LTE: 31c3 +CRC-16/M17: 772b +CRC-16/MAXIM: 44c2 +CRC-16/MAXIM-DOW: 44c2 +CRC-16/MCRF4XX: 6f91 +CRC-16/MODBUS: 4b37 +CRC-16/NRSC-5: a066 +CRC-16/OPENSAFETY-A: 5d38 +CRC-16/OPENSAFETY-B: 20fe +CRC-16/PROFIBUS: a819 +CRC-16/RIELLO: 63d0 +CRC-16/SPI-FUJITSU: e5cc +CRC-16/T10-DIF: d0db +CRC-16/TELEDISK: 0fb3 +CRC-16/TMS37157: 26b1 +CRC-16/UMTS: fee8 +CRC-16/USB: b4c8 +CRC-16/V-41-LSB: 2189 +CRC-16/V-41-MSB: 31c3 +CRC-16/VERIFONE: fee8 +CRC-16/X-25: 906e +CRC-16/XMODEM: 31c3 +CRC-16/ZMODEM: 31c3 +Fletcher-16: 1ede +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["16", true] + } + ] + }, + { + name: "Full generate 17 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-17/CAN-FD: 04f03 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["17", true] + } + ] + }, + { + name: "Full generate 21 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-21/CAN-FD: 0ed841 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["21", true] + } + ] + }, + { + name: "Full generate 24 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-24/BLE: c25a56 +CRC-24/FLEXRAY-A: 7979bd +CRC-24/FLEXRAY-B: 1f23b8 +CRC-24/INTERLAKEN: b4f3e6 +CRC-24/LTE-A: cde703 +CRC-24/LTE-B: 23ef52 +CRC-24/OPENPGP: 21cf02 +CRC-24/OS-9: 200fa5 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["24", true] + } + ] + }, + { + name: "Full generate 30 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-30/CDMA: 04c34abf +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["30", true] + } + ] + }, + { + name: "Full generate 31 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-31/PHILIPS: 0ce9e46c +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["31", true] + } + ] + }, + { + name: "Full generate 32 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `Adler-32: 091e01de +CRC-32: cbf43926 +CRC-32/AAL5: fc891918 +CRC-32/ADCCP: cbf43926 +CRC-32/AIXM: 3010bf7f +CRC-32/AUTOSAR: 1697d06a +CRC-32/BASE91-C: e3069283 +CRC-32/BASE91-D: 87315576 +CRC-32/BZIP2: fc891918 +CRC-32/C: e3069283 +CRC-32/CASTAGNOLI: e3069283 +CRC-32/CD-ROM-EDC: 6ec2edc4 +CRC-32/CKSUM: 765e7680 +CRC-32/D: 87315576 +CRC-32/DECT-B: fc891918 +CRC-32/INTERLAKEN: e3069283 +CRC-32/ISCSI: e3069283 +CRC-32/ISO-HDLC: cbf43926 +CRC-32/JAMCRC: 340bc6d9 +CRC-32/MEF: d2c22f51 +CRC-32/MPEG-2: 0376e6e7 +CRC-32/NVME: e3069283 +CRC-32/PKZIP: cbf43926 +CRC-32/POSIX: 765e7680 +CRC-32/Q: 3010bf7f +CRC-32/SATA: cf72afe8 +CRC-32/V-42: cbf43926 +CRC-32/XFER: bd0be338 +CRC-32/XZ: cbf43926 +Fletcher-32: df09d509 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["32", true] + } + ] + }, + { + name: "Full generate 40 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-40/GSM: d4164fc646 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["40", true] + } + ] + }, + { + name: "Full generate 64 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-64/ECMA-182: 6c40df5f0b497347 +CRC-64/GO-ECMA: 995dc9bbdf1939fa +CRC-64/GO-ISO: b90956c775a41001 +CRC-64/MS: 75d4b74f024eceea +CRC-64/NVME: ae8b14860a799888 +CRC-64/REDIS: e9c6d914c4b8d9ca +CRC-64/WE: 62ec59e3f1a4f00a +CRC-64/XZ: 995dc9bbdf1939fa +Fletcher-64: 0d0803376c6a689f +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["64", true] + } + ] + }, + { + name: "Full generate 82 bits checksums with name", + input: CHECK_STRING, + expectedOutput: `CRC-82/DARC: 09ea83f625023801fd612 +`, + recipeConfig: [ + { + "op": "Generate all checksums", + "args": ["82", true] + } + ] + } +]); diff --git a/tests/operations/tests/GenerateAllHashes.mjs b/tests/operations/tests/GenerateAllHashes.mjs index 8e4a849a..ab4f1b13 100644 --- a/tests/operations/tests/GenerateAllHashes.mjs +++ b/tests/operations/tests/GenerateAllHashes.mjs @@ -54,16 +54,6 @@ LM Hash: 01FC5A6BE7BC6929AAD3B435B51404EE NT Hash: 0CB6948805F797BF2A82807973B89537 SSDEEP: 3:Hn:Hn CTPH: A:E:E - -Checksums: -Fletcher-8: 3d -Fletcher-16: 5dc1 -Fletcher-32: 3f5cd9e7 -Fletcher-64: 7473657474736574 -Adler-32: 045d01c1 -CRC-8: b9 -CRC-16: f82e -CRC-32: d87f7e0c `, recipeConfig: [ { diff --git a/tests/operations/tests/Jsonata.mjs b/tests/operations/tests/Jsonata.mjs new file mode 100644 index 00000000..fb46a961 --- /dev/null +++ b/tests/operations/tests/Jsonata.mjs @@ -0,0 +1,551 @@ +/** + * Jsonata Query tests. + * + * @author Jon King [jon@ajarsoftware.com] + * + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +const INPUT_JSON_OBJECT_WITH_ARRAYS = `{ + "FirstName": "Fred", + "Surname": "Smith", + "Age": 28, + "Address": { + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN" + }, + "Phone": [ + { + "type": "home", + "number": "0203 544 1234" + }, + { + "type": "office", + "number": "01962 001234" + }, + { + "type": "office", + "number": "01962 001235" + }, + { + "type": "mobile", + "number": "077 7700 1234" + } + ], + "Email": [ + { + "type": "work", + "address": ["fred.smith@my-work.com", "fsmith@my-work.com"] + }, + { + "type": "home", + "address": ["freddy@my-social.com", "frederic.smith@very-serious.com"] + } + ], + "Other": { + "Over 18 ?": true, + "Misc": null, + "Alternative.Address": { + "Street": "Brick Lane", + "City": "London", + "Postcode": "E1 6RF" + } + } +}`; + +const INPUT_ARRAY_OF_OBJECTS = `[ + { "ref": [ 1,2 ] }, + { "ref": [ 3,4 ] } +]`; + +const INPUT_NUMBER_ARRAY = `{ + "Numbers": [1, 2.4, 3.5, 10, 20.9, 30] +}`; + +TestRegister.addTests([ + { + name: "Jsonata: Returns a JSON string (double quoted)", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"Smith"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Surname"], + }, + ], + }, + { + name: "Jsonata: Returns a JSON number", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: "28", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Age"], + }, + ], + }, + { + name: "Jsonata: Field references are separated by '.'", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"Winchester"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Address.City"], + }, + ], + }, + { + name: "Jsonata: Matched the path and returns the null value", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: "null", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Other.Misc"], + }, + ], + }, + { + name: "Jsonata: Path not found. Returns nothing", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '""', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Other.DoesntExist"], + }, + ], + }, + { + name: "Jsonata: Field references containing whitespace or reserved tokens can be enclosed in backticks", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Other.`Over 18 ?`"], + }, + ], + }, + { + name: "Jsonata: Returns the first item (an object)", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '{"type":"home","number":"0203 544 1234"}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[0]"], + }, + ], + }, + { + name: "Jsonata: Returns the second item", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '{"type":"office","number":"01962 001234"}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[1]"], + }, + ], + }, + { + name: "Jsonata: Returns the last item", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '{"type":"mobile","number":"077 7700 1234"}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[-1]"], + }, + ], + }, + { + name: "Jsonata: Negative indexed count from the end", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '{"type":"office","number":"01962 001235"}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[-2]"], + }, + ], + }, + { + name: "Jsonata: Doesn't exist - returns nothing", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '""', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[8]"], + }, + ], + }, + { + name: "Jsonata: Selects the number field in the first item", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"0203 544 1234"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[0].number"], + }, + ], + }, + { + name: "Jsonata: No index is given to Phone so it selects all of them (the whole array), then it selects all the number fields for each of them", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: + '["0203 544 1234","01962 001234","01962 001235","077 7700 1234"]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone.number"], + }, + ], + }, + { + name: "Jsonata: Might expect it to just return the first number, but it returns the first number of each of the items selected by Phone", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: + '["0203 544 1234","01962 001234","01962 001235","077 7700 1234"]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone.number[0]"], + }, + ], + }, + { + name: "Jsonata: Applies the index to the array returned by Phone.number.", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"0203 544 1234"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["(Phone.number)[0]"], + }, + ], + }, + { + name: "Jsonata: Returns a range of items by creating an array of indexes", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: + '[{"type":"home","number":"0203 544 1234"},{"type":"office","number":"01962 001234"}]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[[0..1]]"], + }, + ], + }, + // Predicates + { + name: "Jsonata: Select the Phone items that have a type field that equals 'mobile'", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '{"type":"mobile","number":"077 7700 1234"}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[type='mobile']"], + }, + ], + }, + { + name: "Jsonata: Select the mobile phone number", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"077 7700 1234"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[type='mobile'].number"], + }, + ], + }, + { + name: "Jsonata: Select the office phone numbers - there are two of them", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '["01962 001234","01962 001235"]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Phone[type='office'].number"], + }, + ], + }, + // Wildcards + { + name: "Jsonata: Select the values of all the fields of 'Address'", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '["Hursley Park","Winchester","SO21 2JN"]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Address.*"], + }, + ], + }, + { + name: "Jsonata: Select the 'Postcode' value of any child object", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"SO21 2JN"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["*.Postcode"], + }, + ], + }, + { + name: "Jsonata: Select all Postcode values, regardless of how deeply nested they are in the structure", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '["SO21 2JN","E1 6RF"]', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["**.Postcode"], + }, + ], + }, + // String Expressions + { + name: "Jsonata: Concatenate 'FirstName' followed by space followed by 'Surname'", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"Fred Smith"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["FirstName & ' ' & Surname"], + }, + ], + }, + { + name: "Jsonata: Concatenates the 'Street' and 'City' from the 'Address' object with a comma separator", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"Hursley Park, Winchester"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Address.(Street & ', ' & City)"], + }, + ], + }, + { + name: "Jsonata: Casts the operands to strings, if necessary", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: '"50true"', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["5&0&true"], + }, + ], + }, + // Numeric Expressions + { + name: "Jsonata: Addition", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "3.4", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] + Numbers[1]"], + }, + ], + }, + { + name: "Jsonata: Subtraction", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "-19.9", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] - Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Multiplication", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "30", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] * Numbers[5]"], + }, + ], + }, + { + name: "Jsonata: Division", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "0.04784688995215311", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] / Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Modulus", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "3.5", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[2] % Numbers[5]"], + }, + ], + }, + { + name: "Jsonata: Equality", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "false", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] = Numbers[5]"], + }, + ], + }, + { + name: "Jsonata: Inequality", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] != Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Less than", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] < Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Less than or equal to", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] <= Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Greater than", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "false", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[0] > Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Greater than or equal to", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "false", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["Numbers[2] >= Numbers[4]"], + }, + ], + }, + { + name: "Jsonata: Value is contained in", + input: INPUT_JSON_OBJECT_WITH_ARRAYS, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ['"01962 001234" in Phone.number'], + }, + ], + }, + // Boolean Expressions + { + name: "Jsonata: and", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["(Numbers[2] != 0) and (Numbers[5] != Numbers[1])"], + }, + ], + }, + { + name: "Jsonata: or", + input: INPUT_NUMBER_ARRAY, + expectedOutput: "true", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["(Numbers[2] != 0) or (Numbers[5] = Numbers[1])"], + }, + ], + }, + // Array tests + { + name: "Jsonata: $ at the start of an expression refers to the entire input document, subscripting it with 0 selects the first item", + input: INPUT_ARRAY_OF_OBJECTS, + expectedOutput: '{"ref":[1,2]}', + recipeConfig: [ + { + op: "Jsonata Query", + args: ["$[0]"], + }, + ], + }, + { + name: "Jsonata: .ref here returns the entire internal array", + input: INPUT_ARRAY_OF_OBJECTS, + expectedOutput: "[1,2]", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["$[0].ref"], + }, + ], + }, + { + name: "Jsonata: returns element on first position of the internal array", + input: INPUT_ARRAY_OF_OBJECTS, + expectedOutput: "1", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["$[0].ref[0]"], + }, + ], + }, + { + name: "Jsonata: $.field_reference flattens the result into a single array", + input: INPUT_ARRAY_OF_OBJECTS, + expectedOutput: "[1,2,3,4]", + recipeConfig: [ + { + op: "Jsonata Query", + args: ["$.ref"], + }, + ], + }, +]); diff --git a/tests/operations/tests/PHPSerialize.mjs b/tests/operations/tests/PHPSerialize.mjs new file mode 100644 index 00000000..fa6e87c5 --- /dev/null +++ b/tests/operations/tests/PHPSerialize.mjs @@ -0,0 +1,112 @@ +/** + * PHP Serialization tests. + * + * @author brun0ne [brunonblok@gmail.com] + * + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "PHP Serialize empty array", + input: "[]", + expectedOutput: "a:0:{}", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize empty object", + input: "{}", + expectedOutput: "a:0:{}", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize null", + input: "null", + expectedOutput: "N;", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize integer", + input: "10", + expectedOutput: "i:10;", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize float", + input: "14.523", + expectedOutput: "d:14.523;", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize boolean", + input: "[true, false]", + expectedOutput: "a:2:{i:0;b:1;i:1;b:0;}", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize string", + input: "\"Test string to serialize\"", + expectedOutput: "s:24:\"Test string to serialize\";", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize object", + input: "{\"a\": 10,\"0\": {\"ab\": true}}", + expectedOutput: "a:2:{s:1:\"0\";a:1:{s:2:\"ab\";b:1;}s:1:\"a\";i:10;}", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + }, + { + name: "PHP Serialize array", + input: "[1,\"abc\",true,{\"x\":1,\"y\":2}]", + expectedOutput: "a:4:{i:0;i:1;i:1;s:3:\"abc\";i:2;b:1;i:3;a:2:{s:1:\"x\";i:1;s:1:\"y\";i:2;}}", + recipeConfig: [ + { + op: "PHP Serialize", + args: [] + } + ] + } +]); diff --git a/tests/operations/tests/URLEncodeDecode.mjs b/tests/operations/tests/URLEncodeDecode.mjs new file mode 100644 index 00000000..444f76d3 --- /dev/null +++ b/tests/operations/tests/URLEncodeDecode.mjs @@ -0,0 +1,92 @@ +/** + * URLEncode and URLDecode tests. + * + * @author es45411 [135977478+es45411@users.noreply.github.com] + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + // URL Decode + { + name: "URLDecode: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "URL Decode", + args: [], + }, + ], + }, + { + name: "URLDecode: spaces without special chars", + input: "Hello%20world%21", + expectedOutput: "Hello world!", + recipeConfig: [ + { + op: "URL Decode", + args: [], + }, + ], + }, + { + name: "URLDecode: spaces with special chars", + input: "Hello%20world!", + expectedOutput: "Hello world!", + recipeConfig: [ + { + op: "URL Decode", + args: [], + }, + ], + }, + { + name: "URLDecode: decode plus as space", + input: "Hello%20world!", + expectedOutput: "Hello world!", + recipeConfig: [ + { + op: "URL Decode", + args: [], + }, + ], + }, + // URL Encode + { + name: "URLEncode: nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "URL Encode", + args: [], + }, + ], + }, + { + name: "URLEncode: spaces without special chars", + input: "Hello world!", + expectedOutput: "Hello%20world!", + recipeConfig: [ + { + op: "URL Encode", + args: [], + }, + ], + }, + { + name: "URLEncode: spaces with special chars", + input: "Hello world!", + expectedOutput: "Hello%20world%21", + recipeConfig: [ + { + op: "URL Encode", + args: [true], + }, + ], + }, +]); diff --git a/tests/operations/tests/XORChecksum.mjs b/tests/operations/tests/XORChecksum.mjs new file mode 100644 index 00000000..81931630 --- /dev/null +++ b/tests/operations/tests/XORChecksum.mjs @@ -0,0 +1,120 @@ +/** + * Checksum tests. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +const BASIC_STRING = "The ships hung in the sky in much the same way that bricks don't."; +const UTF8_STR = "ნუ პანიკას"; +const ALL_BYTES = [ + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", + "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f", + "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f", + "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f", + "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f", + "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f", + "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f", + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f", + "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf", + "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf", + "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf", + "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef", + "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", +].join(""); + +TestRegister.addTests([ + { + name: "XOR Checksum (1): nothing", + input: "", + expectedOutput: "00", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [1] + } + ] + }, + { + name: "XOR Checksum (1): basic string", + input: BASIC_STRING, + expectedOutput: "08", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [1] + } + ] + }, + { + name: "XOR Checksum (1): UTF-8", + input: UTF8_STR, + expectedOutput: "df", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [1] + } + ] + }, + { + name: "XOR Checksum (1): all bytes", + input: ALL_BYTES, + expectedOutput: "00", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [1] + } + ] + }, + { + name: "XOR Checksum (4): nothing", + input: "", + expectedOutput: "00000000", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [4] + } + ] + }, + { + name: "XOR Checksum (4): basic string", + input: BASIC_STRING, + expectedOutput: "4918421b", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [4] + } + ] + }, + { + name: "XOR Checksum (4): UTF-8", + input: UTF8_STR, + expectedOutput: "83a424dc", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [4] + } + ] + }, + { + name: "XOR Checksum (4): all bytes", + input: ALL_BYTES, + expectedOutput: "00000000", + recipeConfig: [ + { + "op": "XOR Checksum", + "args": [4] + } + ] + }, +]);