Merge remote-tracking branch 'upstream/master' into fix/parse-uri-arguments

This commit is contained in:
mansiverma897993 2026-06-24 18:25:56 +05:30
commit ef7e6ef475
37 changed files with 819 additions and 55 deletions

View File

@ -558,7 +558,7 @@
"Scatter chart",
"Series chart",
"Heatmap chart",
"Extract Audio Metadata"
"Render PDF"
]
},
{

View File

@ -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)];
}
}

View File

@ -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++;
}

View File

@ -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",

View File

@ -27,7 +27,10 @@ class BitShiftLeft extends Operation {
{
"name": "Amount",
"type": "number",
"value": 1
"value": 1,
"min": 0,
"max": 7,
"integer": true,
}
];
}

View File

@ -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("");
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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(

View File

@ -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 {

View File

@ -31,7 +31,8 @@ class PseudoRandomNumberGenerator extends Operation {
{
"name": "Number of bytes",
"type": "number",
"value": 32
"value": 32,
"min": 1
},
{
"name": "Output as",

View 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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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
}
];
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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");

View File

@ -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", () => {

View File

@ -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"]
}
]
},
]);

View 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: [],
},
],
},
]);

View 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],
},
],
},
]);

View File

@ -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: []
}
]
},
]);

View File

@ -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!")'],
},
],
},
]);

View File

@ -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==?=",

View 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"],
},
],
},
]);

View File

@ -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]
}
]
}
]);

View 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"]
}
],
},
]);

View File

@ -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", " "],
},
],
},
]);

View File

@ -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", " "],
},
],
},
]);

View 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"]
},
],
},
]);

View File

@ -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],
},
],
},
]);

View 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"],
},
],
},
]);