From 4a70dff3f2c6aacd8a8bbca42c440082efe88069 Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:01:19 +0100 Subject: [PATCH 01/14] Fix URL encoding incorrectly converting input to UTF-8 (#2340) --- src/core/operations/URLEncode.mjs | 40 ++++++++++++---------- tests/operations/tests/URLEncodeDecode.mjs | 26 ++++++++++++++ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/core/operations/URLEncode.mjs b/src/core/operations/URLEncode.mjs index a5efd213..99eec91d 100644 --- a/src/core/operations/URLEncode.mjs +++ b/src/core/operations/URLEncode.mjs @@ -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.

e.g. = becomes %3d"; 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; } } diff --git a/tests/operations/tests/URLEncodeDecode.mjs b/tests/operations/tests/URLEncodeDecode.mjs index 444f76d3..8d9d09db 100644 --- a/tests/operations/tests/URLEncodeDecode.mjs +++ b/tests/operations/tests/URLEncodeDecode.mjs @@ -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], + }, + ], + }, ]); From ddbe9141322baf88b575ac1615b8e68d3c53b412 Mon Sep 17 00:00:00 2001 From: Shailendra Singh <84718204+Shailendra1703@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:59:40 +0530 Subject: [PATCH 02/14] Added RenderPDF functionality (#2105) Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> very basic unit testing --- src/core/config/Categories.json | 4 +- src/core/operations/RenderPDF.mjs | 100 +++++++++++++++++++++++++++ tests/node/tests/nodeApi.mjs | 2 +- tests/operations/tests/RenderPDF.mjs | 37 ++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/core/operations/RenderPDF.mjs create mode 100644 tests/operations/tests/RenderPDF.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index bce89d4c..2879a13a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -558,7 +558,7 @@ "Scatter chart", "Series chart", "Heatmap chart", - "Extract Audio Metadata" + "Render PDF" ] }, { @@ -603,4 +603,4 @@ "Comment" ] } -] +] \ No newline at end of file diff --git a/src/core/operations/RenderPDF.mjs b/src/core/operations/RenderPDF.mjs new file mode 100644 index 00000000..c1b6cfb5 --- /dev/null +++ b/src/core/operations/RenderPDF.mjs @@ -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 ``; + } + +} + +export default RenderPDF; diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index 2510ef17..5f2476ee 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -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", () => { diff --git a/tests/operations/tests/RenderPDF.mjs b/tests/operations/tests/RenderPDF.mjs new file mode 100644 index 00000000..f359aa20 --- /dev/null +++ b/tests/operations/tests/RenderPDF.mjs @@ -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: /^