Merge remote-tracking branch 'upstream/master' into fix/parse-uri-arguments
This commit is contained in:
commit
ef7e6ef475
@ -558,7 +558,7 @@
|
||||
"Scatter chart",
|
||||
"Series chart",
|
||||
"Heatmap chart",
|
||||
"Extract Audio Metadata"
|
||||
"Render PDF"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@ -108,14 +108,17 @@ export function mean(data) {
|
||||
* @returns {BigNumber}
|
||||
*/
|
||||
export function median(data) {
|
||||
if ((data.length % 2) === 0 && data.length > 0) {
|
||||
if (data.length > 0) {
|
||||
data.sort(function(a, b) {
|
||||
return a.minus(b);
|
||||
});
|
||||
|
||||
if ((data.length % 2) === 0) {
|
||||
const first = data[Math.floor(data.length / 2)];
|
||||
const second = data[Math.floor(data.length / 2) - 1];
|
||||
return mean([first, second]);
|
||||
} else {
|
||||
}
|
||||
|
||||
return data[Math.floor(data.length / 2)];
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,20 +33,29 @@ export default class TLVParser {
|
||||
* @returns {number}
|
||||
*/
|
||||
getLength() {
|
||||
let bytesInLength = this.bytesInLength;
|
||||
let bigEndian = false;
|
||||
|
||||
if (this.basicEncodingRules) {
|
||||
const bit = this.input[this.location];
|
||||
if (bit & 0x80) {
|
||||
this.bytesInLength = bit & ~0x80;
|
||||
} else {
|
||||
const firstLengthByte = this.input[this.location];
|
||||
this.location++;
|
||||
return bit & ~0x80;
|
||||
|
||||
if (firstLengthByte & 0x80) {
|
||||
bytesInLength = firstLengthByte & ~0x80;
|
||||
bigEndian = true;
|
||||
} else {
|
||||
return firstLengthByte & ~0x80;
|
||||
}
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
|
||||
for (let i = 0; i < this.bytesInLength; i++) {
|
||||
for (let i = 0; i < bytesInLength; i++) {
|
||||
if (bigEndian) {
|
||||
length = (length << 8) + this.input[this.location];
|
||||
} else {
|
||||
length += this.input[this.location] * Math.pow(Math.pow(2, 8), i);
|
||||
}
|
||||
this.location++;
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,11 @@ class BLAKE3 extends Operation {
|
||||
this.args = [
|
||||
{
|
||||
"name": "Size (bytes)",
|
||||
"type": "number"
|
||||
"type": "number",
|
||||
"value": 16,
|
||||
"min": 1,
|
||||
"max": 65535, // arbitrary limit to prevent resource exhaustion
|
||||
"integer": true,
|
||||
}, {
|
||||
"name": "Key",
|
||||
"type": "string",
|
||||
|
||||
@ -27,7 +27,10 @@ class BitShiftLeft extends Operation {
|
||||
{
|
||||
"name": "Amount",
|
||||
"type": "number",
|
||||
"value": 1
|
||||
"value": 1,
|
||||
"min": 0,
|
||||
"max": 7,
|
||||
"integer": true,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -45,12 +45,15 @@ class DechunkHTTPResponse extends Operation {
|
||||
const lineEndingsLength = lineEndings.length;
|
||||
let chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16);
|
||||
while (!isNaN(chunkSize)) {
|
||||
if (chunkSize === 0) {
|
||||
break;
|
||||
}
|
||||
chunks.push(input.slice(chunkSizeEnd, chunkSize + chunkSizeEnd));
|
||||
input = input.slice(chunkSizeEnd + chunkSize + lineEndingsLength);
|
||||
chunkSizeEnd = input.indexOf(lineEndings) + lineEndingsLength;
|
||||
chunkSize = parseInt(input.slice(0, chunkSizeEnd), 16);
|
||||
}
|
||||
return chunks.join("") + input;
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -51,9 +51,10 @@ class FromBase extends Operation {
|
||||
if (number.length === 1) return result;
|
||||
|
||||
// Fractional part
|
||||
const radixBN = new BigNumber(radix);
|
||||
for (let i = 0; i < number[1].length; i++) {
|
||||
const digit = new BigNumber(number[1][i], radix);
|
||||
result += digit.div(Math.pow(radix, i+1));
|
||||
result = result.plus(digit.div(radixBN.pow(i + 1)));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@ -74,13 +74,11 @@ class Gzip extends Operation {
|
||||
}
|
||||
if (comment.length) {
|
||||
options.flags.comment = true;
|
||||
options.flags.fcomment = true;
|
||||
options.comment = comment;
|
||||
}
|
||||
const gzipObj = new Zlib.Gzip(new Uint8Array(input), options);
|
||||
const compressed = new Uint8Array(gzipObj.compress());
|
||||
if (options.flags.comment && !(compressed[3] & 0x10)) {
|
||||
compressed[3] |= 0x10;
|
||||
}
|
||||
return compressed.buffer;
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,18 @@ class JsonataQuery extends Operation {
|
||||
|
||||
try {
|
||||
const expression = jsonata(query);
|
||||
// Override built-in base64 functions which fail in Web Worker
|
||||
// context where `window` is undefined. The jsonata library falls
|
||||
// back to `global.Buffer` which also does not exist in workers.
|
||||
// `atob`/`btoa` are available in both browser and worker scopes.
|
||||
expression.registerFunction("base64decode", (str) => {
|
||||
if (typeof str === "undefined") return undefined;
|
||||
return atob(str);
|
||||
}, "<s-:s>");
|
||||
expression.registerFunction("base64encode", (str) => {
|
||||
if (typeof str === "undefined") return undefined;
|
||||
return btoa(str);
|
||||
}, "<s-:s>");
|
||||
result = await expression.evaluate(jsonObj);
|
||||
} catch (err) {
|
||||
throw new OperationError(
|
||||
|
||||
@ -87,7 +87,7 @@ class MIMEDecoding extends Operation {
|
||||
end = cur + j + "?=".length;
|
||||
|
||||
if (encoding.toLowerCase() === "b") {
|
||||
text = fromBase64(text);
|
||||
text = fromBase64(text, undefined, "byteArray");
|
||||
} else if (encoding.toLowerCase() === "q") {
|
||||
text = this.parseQEncodedWord(text);
|
||||
} else {
|
||||
|
||||
@ -31,7 +31,8 @@ class PseudoRandomNumberGenerator extends Operation {
|
||||
{
|
||||
"name": "Number of bytes",
|
||||
"type": "number",
|
||||
"value": 32
|
||||
"value": 32,
|
||||
"min": 1
|
||||
},
|
||||
{
|
||||
"name": "Output as",
|
||||
|
||||
100
src/core/operations/RenderPDF.mjs
Normal file
100
src/core/operations/RenderPDF.mjs
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @author Shailendra [singhshailendra.in]
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import { fromBase64, toBase64 } from "../lib/Base64.mjs";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import Utils from "../Utils.mjs";
|
||||
|
||||
/**
|
||||
* Render PDF operation
|
||||
*/
|
||||
class RenderPDF extends Operation {
|
||||
|
||||
/**
|
||||
* RenderPDF constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Render PDF";
|
||||
this.module = "File";
|
||||
this.description = "Displays the input as a PDF preview. Supports Raw and Base64 input formats.";
|
||||
this.inputType = "string";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Input format",
|
||||
"type": "option",
|
||||
"value": ["Base64", "Raw"],
|
||||
}
|
||||
];
|
||||
this.checks = [
|
||||
{
|
||||
pattern: "^%PDF-",
|
||||
flags: "",
|
||||
args: ["Raw"],
|
||||
useful: true,
|
||||
output: {
|
||||
mime: "application/pdf"
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
run(input, args) {
|
||||
const inputFormat = args[0];
|
||||
|
||||
if (!input.length) return [];
|
||||
|
||||
// Convert input to raw bytes
|
||||
switch (inputFormat) {
|
||||
case "Base64":
|
||||
input = fromBase64(input, undefined, "byteArray");
|
||||
break;
|
||||
case "Raw":
|
||||
default:
|
||||
input = Utils.strToByteArray(input);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check PDF signature
|
||||
if (
|
||||
input[0] !== 0x25 || // %
|
||||
input[1] !== 0x50 || // P
|
||||
input[2] !== 0x44 || // D
|
||||
input[3] !== 0x46 // F
|
||||
) {
|
||||
throw new OperationError("Input does not appear to be a PDF file.");
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the PDF using HTML for web apps.
|
||||
*
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
async present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const base64 = toBase64(data);
|
||||
const dataURI = "data:application/pdf;base64," + base64;
|
||||
|
||||
return `<iframe src="${dataURI}" style="width:100%;height:100%;border:1px solid #ccc;"></iframe>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RenderPDF;
|
||||
@ -75,9 +75,16 @@ class SetDifference extends Operation {
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
runSetDifference(a, b) {
|
||||
const excluded = new Set(b);
|
||||
const seen = new Set();
|
||||
|
||||
return a
|
||||
.filter((item) => {
|
||||
return b.indexOf(item) === -1;
|
||||
if (excluded.has(item) || seen.has(item)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(item);
|
||||
return true;
|
||||
})
|
||||
.join(this.itemDelimiter);
|
||||
}
|
||||
|
||||
@ -75,9 +75,16 @@ class SetIntersection extends Operation {
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
runIntersect(a, b) {
|
||||
const included = new Set(b);
|
||||
const seen = new Set();
|
||||
|
||||
return a
|
||||
.filter((item) => {
|
||||
return b.indexOf(item) > -1;
|
||||
if (!included.has(item) || seen.has(item)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(item);
|
||||
return true;
|
||||
})
|
||||
.join(this.itemDelimiter);
|
||||
}
|
||||
|
||||
@ -71,6 +71,16 @@ class ShowOnMap extends Operation {
|
||||
}
|
||||
latLong = latLong.replace(/[,]$/, "");
|
||||
latLong = latLong.replace(/°/g, "");
|
||||
|
||||
// The map requires a latitude and longitude pair. If the conversion only produced a
|
||||
// single value (e.g. because the chosen input delimiter didn't match the input), bail
|
||||
// out with a helpful message rather than passing it on to the map, which would throw an
|
||||
// uncaught TypeError in the browser.
|
||||
const coords = latLong.split(",").map(v => v.trim());
|
||||
if (coords.length !== 2 || coords.some(v => v === "" || isNaN(Number(v)))) {
|
||||
throw new OperationError(`Could not show coordinates '${latLong}' on the map. Expected a latitude and longitude pair - check that the input format and delimiter are correct.`);
|
||||
}
|
||||
|
||||
return latLong;
|
||||
}
|
||||
return input;
|
||||
|
||||
@ -28,7 +28,10 @@ class ToBase extends Operation {
|
||||
{
|
||||
"name": "Radix",
|
||||
"type": "number",
|
||||
"value": 36
|
||||
"value": 36,
|
||||
"min": 2,
|
||||
"max": 36,
|
||||
"integer": true,
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -43,9 +46,6 @@ class ToBase extends Operation {
|
||||
throw new OperationError("Error: Input must be a number");
|
||||
}
|
||||
const radix = args[0];
|
||||
if (radix < 2 || radix > 36) {
|
||||
throw new OperationError("Error: Radix argument must be between 2 and 36");
|
||||
}
|
||||
return input.toString(radix);
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,10 @@ class ToBinary extends Operation {
|
||||
{
|
||||
"name": "Byte Length",
|
||||
"type": "number",
|
||||
"value": 8
|
||||
"value": 8,
|
||||
"min": 1,
|
||||
"max": 256, // arbitrary - significantly larger than word size for any known machine ("640k ought to be enough for anybody")
|
||||
"integer": true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ class URLEncode extends Operation {
|
||||
this.module = "URL";
|
||||
this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.<br><br>e.g. <code>=</code> becomes <code>%3d</code>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Percent-encoding";
|
||||
this.inputType = "string";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
@ -33,34 +33,38 @@ class URLEncode extends Operation {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const encodeAll = args[0];
|
||||
return encodeAll ? this.encodeAllChars(input) : encodeURI(input);
|
||||
return this.encodeBytes(input, encodeAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode characters in URL outside of encodeURI() function spec
|
||||
* Encode bytes in URL using percent encoding.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {byteArray} bytes
|
||||
* @param {boolean} encodeAll
|
||||
* @returns {string}
|
||||
*/
|
||||
encodeAllChars (str) {
|
||||
// TODO Do this programmatically
|
||||
return encodeURIComponent(str)
|
||||
.replace(/!/g, "%21")
|
||||
.replace(/#/g, "%23")
|
||||
.replace(/'/g, "%27")
|
||||
.replace(/\(/g, "%28")
|
||||
.replace(/\)/g, "%29")
|
||||
.replace(/\*/g, "%2A")
|
||||
.replace(/-/g, "%2D")
|
||||
.replace(/\./g, "%2E")
|
||||
.replace(/_/g, "%5F")
|
||||
.replace(/~/g, "%7E");
|
||||
encodeBytes(bytes, encodeAll) {
|
||||
const safeChars = encodeAll ?
|
||||
/^[A-Za-z0-9]$/ :
|
||||
/^[A-Za-z0-9:/?#[\]@!$&'()*+,;=%]$/;
|
||||
|
||||
let output = "";
|
||||
|
||||
for (const byte of bytes) {
|
||||
const char = String.fromCharCode(byte);
|
||||
|
||||
output += safeChars.test(char) ?
|
||||
char :
|
||||
"%" + byte.toString(16).toUpperCase().padStart(2, "0");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -56,7 +56,8 @@ class UnescapeUnicodeCharacters extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const prefix = prefixToRegex[args[0]],
|
||||
regex = new RegExp(prefix+"([a-f\\d]{4})", "ig");
|
||||
quantifier = args[0] === "U+" ? "{4,6}" : "{4}",
|
||||
regex = new RegExp(prefix+"([a-f\\d]"+quantifier+")", "ig");
|
||||
let output = "",
|
||||
m,
|
||||
i = 0;
|
||||
|
||||
@ -31,7 +31,10 @@ class XORBruteForce extends Operation {
|
||||
{
|
||||
"name": "Key length",
|
||||
"type": "number",
|
||||
"value": 1
|
||||
"value": 1,
|
||||
"min": 1,
|
||||
"max": 2,
|
||||
"integer": true
|
||||
},
|
||||
{
|
||||
"name": "Sample length",
|
||||
|
||||
@ -218,6 +218,7 @@ module.exports = {
|
||||
testOpHtml(browser, "JSON Beautify", "{a:1}", ".json-dict .json-literal", "1");
|
||||
// testOp(browser, "JSON Minify", "test input", "test_output");
|
||||
// testOp(browser, "JSON to CSV", "test input", "test_output");
|
||||
testOp(browser, "Jsonata Query", '{"a": "SGVsbG8gV29ybGQh"}', '"Hello World!"', ["$base64decode($.a)"]);
|
||||
// testOp(browser, "JWT Decode", "test input", "test_output");
|
||||
// testOp(browser, "JWT Sign", "test input", "test_output");
|
||||
// testOp(browser, "JWT Verify", "test input", "test_output");
|
||||
|
||||
@ -136,7 +136,7 @@ TestRegister.addApiTests([
|
||||
|
||||
it("chef.help: returns multiple results", () => {
|
||||
const result = chef.help("base 64");
|
||||
assert.strictEqual(result.length, 13);
|
||||
assert.strictEqual(result.length, 14);
|
||||
}),
|
||||
|
||||
it("chef.help: looks in description for matches too", () => {
|
||||
|
||||
@ -69,5 +69,43 @@ TestRegister.addTests([
|
||||
{ "op": "BLAKE3",
|
||||
"args": [16390, "ThiskeyisexactlythirtytwoBytesLo"] }
|
||||
]
|
||||
},
|
||||
// test vectors from https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json
|
||||
{
|
||||
name: "BLAKE3: Std test vector - 0 bytes input, plain hash",
|
||||
input: "",
|
||||
expectedOutput: "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "BLAKE3",
|
||||
"args": [131, ""]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "BLAKE3: Std test vector - 0 bytes input, keyed hash",
|
||||
input: "",
|
||||
expectedOutput: "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "BLAKE3",
|
||||
"args": [131, "whats the Elvish word for friend"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "BLAKE3: Std test vector - 7 bytes input, keyed hash",
|
||||
input: "0001020304050607",
|
||||
expectedOutput: "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "From Hex",
|
||||
args: [],
|
||||
},
|
||||
{
|
||||
"op": "BLAKE3",
|
||||
"args": [131, "whats the Elvish word for friend"]
|
||||
}
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
66
tests/operations/tests/DechunkHTTPResponse.mjs
Normal file
66
tests/operations/tests/DechunkHTTPResponse.mjs
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* DechunkHTTPResponse operation tests.
|
||||
*
|
||||
* @author Willi Ballenthin
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "Dechunk HTTP response: CRLF line endings",
|
||||
input: "7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n",
|
||||
expectedOutput: "MozillaDeveloperNetwork",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Dechunk HTTP response",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dechunk HTTP response: LF line endings",
|
||||
input: "7\nMozilla\n9\nDeveloper\n7\nNetwork\n0\n\n",
|
||||
expectedOutput: "MozillaDeveloperNetwork",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Dechunk HTTP response",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dechunk HTTP response: single chunk",
|
||||
input: "5\r\nHello\r\n0\r\n\r\n",
|
||||
expectedOutput: "Hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Dechunk HTTP response",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dechunk HTTP response: trailing headers discarded",
|
||||
input: "7\nMozilla\n9\nDeveloper\n7\nNetwork\n0\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\n",
|
||||
expectedOutput: "MozillaDeveloperNetwork",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Dechunk HTTP response",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dechunk HTTP response: hex chunk sizes",
|
||||
input: "a\r\n0123456789\r\n0\r\n\r\n",
|
||||
expectedOutput: "0123456789",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Dechunk HTTP response",
|
||||
args: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
66
tests/operations/tests/FromBase.mjs
Normal file
66
tests/operations/tests/FromBase.mjs
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* From Base operation tests.
|
||||
*
|
||||
* @author Willi Ballenthin
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "From Base: binary integer",
|
||||
input: "1010",
|
||||
expectedOutput: "10",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Base",
|
||||
args: [2],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Base: binary fraction",
|
||||
input: "10.1",
|
||||
expectedOutput: "2.5",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Base",
|
||||
args: [2],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Base: hex fraction",
|
||||
input: "a.8",
|
||||
expectedOutput: "10.5",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Base",
|
||||
args: [16],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Base: octal integer",
|
||||
input: "77",
|
||||
expectedOutput: "63",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Base",
|
||||
args: [8],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "From Base: octal fraction",
|
||||
input: "7.4",
|
||||
expectedOutput: "7.5",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Base",
|
||||
args: [8],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -86,4 +86,64 @@ TestRegister.addTests([
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gzip: Comment with checksum round-trips through Gunzip",
|
||||
input: "hello hello hello",
|
||||
expectedOutput: "hello hello hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Gzip",
|
||||
args: ["Dynamic Huffman Coding", "", "test", true]
|
||||
},
|
||||
{
|
||||
op: "Gunzip",
|
||||
args: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gzip: Filename and comment with checksum round-trips through Gunzip",
|
||||
input: "The quick brown fox jumped over the slow dog",
|
||||
expectedOutput: "The quick brown fox jumped over the slow dog",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Gzip",
|
||||
args: ["Dynamic Huffman Coding", "file.txt", "a comment", true]
|
||||
},
|
||||
{
|
||||
op: "Gunzip",
|
||||
args: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gzip: No comment, with checksum round-trips through Gunzip",
|
||||
input: "The quick brown fox jumped over the slow dog",
|
||||
expectedOutput: "The quick brown fox jumped over the slow dog",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Gzip",
|
||||
args: ["Dynamic Huffman Coding", "", "", true]
|
||||
},
|
||||
{
|
||||
op: "Gunzip",
|
||||
args: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gzip: No options round-trips through Gunzip",
|
||||
input: "The quick brown fox jumped over the slow dog",
|
||||
expectedOutput: "The quick brown fox jumped over the slow dog",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Gzip",
|
||||
args: ["Dynamic Huffman Coding", "", "", false]
|
||||
},
|
||||
{
|
||||
op: "Gunzip",
|
||||
args: []
|
||||
}
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
@ -548,4 +548,27 @@ TestRegister.addTests([
|
||||
},
|
||||
],
|
||||
},
|
||||
// Base64 functions (issue #2063)
|
||||
{
|
||||
name: "Jsonata: $base64decode",
|
||||
input: "{}",
|
||||
expectedOutput: '"Hello World!"',
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Jsonata Query",
|
||||
args: ['$base64decode("SGVsbG8gV29ybGQh")'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Jsonata: $base64encode",
|
||||
input: "{}",
|
||||
expectedOutput: '"SGVsbG8gV29ybGQh"',
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Jsonata Query",
|
||||
args: ['$base64encode("Hello World!")'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@ -75,6 +75,39 @@ TestRegister.addTests([
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "UTF-8 Base64 non-ASCII",
|
||||
input: "Subject: =?UTF-8?B?Y2Fmw6k=?=",
|
||||
expectedOutput: "Subject: café",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "MIME Decoding",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "UTF-8 Base64 multibyte CJK",
|
||||
input: "Subject: =?UTF-8?B?5pel5pys6Kqe?=",
|
||||
expectedOutput: "Subject: 日本語",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "MIME Decoding",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "UTF-8 Base64 ASCII-only",
|
||||
input: "Subject: =?UTF-8?B?aGVsbG8=?=",
|
||||
expectedOutput: "Subject: hello",
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "MIME Decoding",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "ISO Decoding",
|
||||
input: "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\nTo: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\nCC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\nSubject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=",
|
||||
|
||||
33
tests/operations/tests/Median.mjs
Normal file
33
tests/operations/tests/Median.mjs
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Median operation tests.
|
||||
*
|
||||
* @author copilot-swe-agent[bot]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "Median: odd-length input",
|
||||
input: "10 1 2",
|
||||
expectedOutput: "2",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Median",
|
||||
args: ["Space"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Median: even-length input",
|
||||
input: "10 1 2 5",
|
||||
expectedOutput: "3.5",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Median",
|
||||
args: ["Space"],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -52,5 +52,46 @@ TestRegister.addTests([
|
||||
"args": [1, 4, true] // length value is patently wrong, should be ignored by BER.
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parse TLV: BER long-form length (two-byte length encoding)",
|
||||
input: "\x01\x82\x01\x00" + "A".repeat(256) + "\x02\x03\x41\x42\x43",
|
||||
expectedOutput: JSON.stringify([
|
||||
{"key": [1], "length": 256, "value": Array(256).fill(65)},
|
||||
{"key": [2], "length": 3, "value": [65, 66, 67]}
|
||||
], null, 4),
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "Parse TLV",
|
||||
"args": [1, 1, true]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parse TLV: BER long-form length (one-byte length encoding)",
|
||||
input: "\x01\x81\x80" + "B".repeat(128),
|
||||
expectedOutput: JSON.stringify([
|
||||
{"key": [1], "length": 128, "value": Array(128).fill(66)}
|
||||
], null, 4),
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "Parse TLV",
|
||||
"args": [1, 1, true]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parse TLV: BER multiple entries with mixed short and long-form lengths",
|
||||
input: "\x01\x05\x48\x65\x6c\x6c\x6f\x02\x81\x05\x57\x6f\x72\x6c\x64",
|
||||
expectedOutput: JSON.stringify([
|
||||
{"key": [1], "length": 5, "value": [72, 101, 108, 108, 111]},
|
||||
{"key": [2], "length": 5, "value": [87, 111, 114, 108, 100]}
|
||||
], null, 4),
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "Parse TLV",
|
||||
"args": [1, 1, true]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
37
tests/operations/tests/RenderPDF.mjs
Normal file
37
tests/operations/tests/RenderPDF.mjs
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* RenderPDF tests.
|
||||
*
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "RenderPDF",
|
||||
input: "Not a PDF",
|
||||
expectedOutput: "Input does not appear to be a PDF file.",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Render PDF",
|
||||
args: ["Raw"]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "RenderPDF",
|
||||
input: "",
|
||||
expectedMatch: /^<iframe src="data:application\/pdf;base64,JVBERi0xLjAKCjEgMCBvYmogPDwg/,
|
||||
recipeConfig: [
|
||||
{
|
||||
"op": "Generate QR Code",
|
||||
"args": ["PDF", 1, 1, "Low"]
|
||||
},
|
||||
{
|
||||
"op": "Render PDF",
|
||||
"args": ["Raw"]
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -53,4 +53,26 @@ TestRegister.addTests([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Set Difference: duplicates in first set are removed",
|
||||
input: "red,red,blue\n\nblue",
|
||||
expectedOutput: "red",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Set Difference",
|
||||
args: ["\n\n", ","],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Set Difference: duplicates in both sets",
|
||||
input: "1 1 2 2 3\n\n2 2 3 3",
|
||||
expectedOutput: "1",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Set Difference",
|
||||
args: ["\n\n", " "],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@ -52,5 +52,27 @@ TestRegister.addTests([
|
||||
args: ["z", "-"],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Set Intersection: duplicates in first set are removed",
|
||||
input: "red,red,blue\n\nred,blue",
|
||||
expectedOutput: "red,blue",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Set Intersection",
|
||||
args: ["\n\n", ","],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Set Intersection: duplicates in both sets",
|
||||
input: "1 1 2 2 3\n\n2 2 3 3 4",
|
||||
expectedOutput: "2 3",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Set Intersection",
|
||||
args: ["\n\n", " "],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
39
tests/operations/tests/ShowOnMap.mjs
Normal file
39
tests/operations/tests/ShowOnMap.mjs
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Show on map tests
|
||||
*
|
||||
* @author Leon Zandman [leon@wirwar.com]
|
||||
*
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "Show on map: valid coordinate pair",
|
||||
input: "51.5007, -0.1246",
|
||||
// The presented output is the Leaflet map HTML; just check the coordinates made it through.
|
||||
expectedMatch: /51\.5007,-0\.1246/,
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Show on map",
|
||||
args: [13, "Auto", "Auto"]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Regression test: a comma-separated input with the delimiter set to "\n" used to be
|
||||
// mis-detected as a single Degrees Decimal Minutes value (1° 24' = 1.4°), producing a single
|
||||
// coordinate. That single value was then passed to Leaflet's setView([1.4], ...), throwing
|
||||
// an uncaught "Cannot read properties of null (reading 'lat')" TypeError in the browser.
|
||||
name: "Show on map: single value is rejected with a helpful error",
|
||||
input: "1, 24",
|
||||
expectedOutput: "Could not show coordinates '1.4' on the map. Expected a latitude and longitude pair - check that the input format and delimiter are correct.",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Show on map",
|
||||
args: [13, "Auto", "\\n"]
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -89,4 +89,30 @@ TestRegister.addTests([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "URLEncode: encodes UTF-8 text as UTF-8 bytes",
|
||||
input: "你好",
|
||||
expectedOutput: "%E4%BD%A0%E5%A5%BD",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "URL Encode",
|
||||
args: [false],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "URLEncode: preserves raw bytes from From Hex",
|
||||
input: "6c6567697466696c6580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090746869737761737375706f736564746f6265616e6578706c6f6974",
|
||||
expectedOutput: "legitfile%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90thiswassuposedtobeanexploit",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "From Hex",
|
||||
args: ["None"],
|
||||
},
|
||||
{
|
||||
op: "URL Encode",
|
||||
args: [false],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
88
tests/operations/tests/UnescapeUnicodeCharacters.mjs
Normal file
88
tests/operations/tests/UnescapeUnicodeCharacters.mjs
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Unescape Unicode Characters operation tests.
|
||||
*
|
||||
* @author williballenthin
|
||||
* @copyright Crown Copyright 2024
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
name: "Unescape Unicode Characters: \\u 4-digit BMP",
|
||||
input: "\\u03c3\\u03bf\\u03c5",
|
||||
expectedOutput: "σου",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["\\u"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: %u 4-digit BMP",
|
||||
input: "%u03c3%u03bf%u03c5",
|
||||
expectedOutput: "σου",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["%u"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: U+ 4-digit BMP",
|
||||
input: "U+0041",
|
||||
expectedOutput: "A",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["U+"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: U+ 5-digit astral plane emoji",
|
||||
input: "U+1F600",
|
||||
expectedOutput: "\u{1F600}",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["U+"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: U+ 6-digit zero-padded",
|
||||
input: "U+000041",
|
||||
expectedOutput: "A",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["U+"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: U+ mixed lengths",
|
||||
input: "U+0041 U+1F600 U+000042",
|
||||
expectedOutput: "A \u{1F600} B",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["U+"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Unescape Unicode Characters: passthrough with no matches",
|
||||
input: "hello world",
|
||||
expectedOutput: "hello world",
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Unescape Unicode Characters",
|
||||
args: ["\\u"],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user