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: /^