Merge branch 'master' into she-key-update

This commit is contained in:
Parman Mohammadalizadeh 2026-07-26 19:35:32 +03:30 committed by GitHub
commit 353890fd82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 425 additions and 16 deletions

View File

@ -193,6 +193,14 @@
"xregexp": "^5.1.2",
"zlibjs": "^0.3.1"
},
"allowScripts": {
"@nightwatch/nightwatch-inspector@1.0.1": true,
"chromedriver@150.0.3": true,
"core-js": false,
"core-js-pure": false,
"fsevents": false,
"tesseract.js": false
},
"scripts": {
"start": "npx grunt dev",
"build": "npx grunt prod",

View File

@ -86,6 +86,9 @@ class Ingredient {
if (this.type === "option" && Array.isArray(checkVal)) {
checkVal = checkVal[this.defaultIndex ?? 0];
}
if (this.type === "argSelector" && Array.isArray(checkVal)) {
checkVal = checkVal[this.defaultIndex ?? 0]?.name || "";
}
// 1. check if empty
let isEmpty = false;
@ -99,8 +102,10 @@ class Ingredient {
let isAllowedOptionEmpty = false;
if (this.type === "option" && Array.isArray(this.defaultValue)) {
isAllowedOptionEmpty = this.defaultValue.includes("");
} else if (this.type === "argSelector" && Array.isArray(this.defaultValue)) {
isAllowedOptionEmpty = this.defaultValue.some(opt => opt.name === "");
}
if (this.allowEmpty === false || (this.type === "option" && !isAllowedOptionEmpty)) {
if (this.allowEmpty === false || ((this.type === "option" || this.type === "argSelector") && !isAllowedOptionEmpty)) {
throw new OperationError(`${this.name} cannot be empty.`);
}
return true;
@ -150,6 +155,23 @@ class Ingredient {
}
}
// 5. argSelector checks
if (this.type === "argSelector") {
if (Array.isArray(this.defaultValue)) {
const permittedOptions = this.defaultValue
.map(opt => opt.name)
.filter(optName => {
if (typeof optName !== "string") return false;
return !optName.match(/^\[\/?[a-z0-9 -()^]+\]$/i);
});
const valStr = (checkVal !== null && checkVal !== undefined) ? String(checkVal).toLowerCase() : "";
const matchedOption = permittedOptions.find(opt => opt.toLowerCase() === valStr);
if (!matchedOption) {
throw new OperationError(`${this.name} must be one of the following: ${permittedOptions.join(", ")}.`);
}
}
}
return true;
}

View File

@ -247,6 +247,7 @@
"Divide",
"MOD",
"Extended GCD",
"Modular Exponentiation",
"Modular Inverse",
"Mean",
"Median",

View File

@ -35,32 +35,32 @@ class A1Z26CipherDecode extends Operation {
];
this.checks = [
{
pattern: "^\\s*([12]?[0-9] )+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]) )+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["Space"]
},
{
pattern: "^\\s*([12]?[0-9],)+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]),)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["Comma"]
},
{
pattern: "^\\s*([12]?[0-9];)+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]);)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["Semi-colon"]
},
{
pattern: "^\\s*([12]?[0-9]:)+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6]):)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["Colon"]
},
{
pattern: "^\\s*([12]?[0-9]\\n)+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6])\\n)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["Line feed"]
},
{
pattern: "^\\s*([12]?[0-9]\\r\\n)+[12]?[0-9]\\s*$",
pattern: "^\\s*((?:0?[1-9]|1[0-9]|2[0-6])\\r\\n)+(?:0?[1-9]|1[0-9]|2[0-6])\\s*$",
flags: "",
args: ["CRLF"]
}

View File

@ -66,6 +66,23 @@ class AutomatedValidationTestOp extends Operation {
"type": "option",
"value": ["[Group 1]", "Option 1", "Option 2", "[/Group 1]", "[Group 2]", "Option 3", "[/Group 2]"],
"allowEmpty": false
},
{
"name": "Arg Selector Ingredient",
"type": "argSelector",
"value": [
{
name: "Option 1",
on: [0],
off: [1]
},
{
name: "Option 2",
on: [1],
off: [0]
}
],
"allowEmpty": false
}
];
}

View File

@ -0,0 +1,111 @@
/**
* @author p-leriche [philip.leriche@cantab.net]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { parseBigInt, modPow } from "../lib/BigIntUtils.mjs";
/* ---------- operation class ---------- */
/**
* Modular Exponentiation operation
*/
class ModularExponentiation extends Operation {
/**
* ModularExponentiation constructor
*/
constructor() {
super();
this.name = "Modular Exponentiation";
this.module = "Crypto";
this.description = "Performs modular exponentiation, as used in Diffie-Hellman and RSA.<br><br>" +
"Computes Base ^ Exponent mod Modulus.<br><br>" +
"<b>Input handling:</b> If <i>either</i> Base <i>or</i> Exponent is left blank, " +
"its value is taken from the Input field.";
this.infoURL = "https://wikipedia.org/wiki/Modular_exponentiation";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Base",
type: "string",
value: ""
},
{
name: "Modulus",
type: "string",
value: "1"
},
{
name: "Exponent",
type: "string",
value: ""
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [baseStr, modStr, expStr] = args;
// Trim everything so "" and " " count as empty
const baseParam = baseStr?.trim();
const expParam = expStr?.trim();
const modParam = modStr?.trim();
const inputVal = input?.trim();
const mod = modParam;
if (!mod) {
throw new OperationError("Modulus must be defined");
}
// Base *or* Exponent (but not both) are taken from the Input
// if their boxes are empty.
let base, exp;
if (baseParam && expParam) {
// Case 1: base and exponent both given as parameters
base = baseParam;
exp = expParam;
} else if (!baseParam && expParam) {
// Case 2: base missing - take from input
base = inputVal;
exp = expParam;
if (!base) {
throw new OperationError("Base must be defined");
}
} else if (baseParam && !expParam) {
// Case 3: exponent missing - take from input
base = baseParam;
exp = inputVal;
if (!exp) {
throw new OperationError("Exponent must be defined");
}
} else if (!inputVal) {
// Case 4: base and exponent both missing
throw new OperationError("Base and Exponent must be defined");
} else throw new OperationError("Ambiguous input: specify either Base or Exponent when using Input");
// Parse numbers
const baseBI = parseBigInt(base, "Base");
const expBI = parseBigInt(exp, "Exponent");
const modBI = parseBigInt(mod, "Modulus");
// Check for invalid modulus (parseBigInt eliminates negatives)
if (modBI === 0n) {
throw new OperationError("Modulus must be greater than zero");
}
return modPow(baseBI, expBI, modBI).toString();
}
}
export default ModularExponentiation;

View File

@ -33,15 +33,11 @@ class ParseQRCode extends Operation {
value: false,
},
];
this.checks = [
{
pattern:
"^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)",
flags: "",
args: [false],
useful: true,
},
];
// No Magic checks: detecting a QR code in arbitrary image data requires
// actually attempting to parse one, which is expensive and produces
// spurious "Could not read a QR code from the image" log messages for
// any image input via Magic. Users can add Parse QR Code manually when
// they know the image contains a QR code. See issue #2610.
}
/**

View File

@ -77,6 +77,10 @@ function transformArgs(opArgsList, newArgs) {
return !Array.isArray(arg.value) ? arg.value : arg.value[arg.defaultIndex ?? 0];
}
if (arg.type === "argSelector") {
return !Array.isArray(arg.value) ? arg.value : (arg.value[arg.defaultIndex ?? 0]?.name || "");
}
if (arg.type === "editableOption") {
return typeof arg.value === "string" ? arg.value : arg.value[arg.defaultIndex ?? 0].value;
}

View File

@ -27,6 +27,7 @@ import "./tests/Categories.mjs";
import "./tests/ToHTMLEntity.mjs";
import "./tests/lib/BigIntUtils.mjs";
import "./tests/lib/ChartsProtocolPrototypePollution.mjs";
import "./tests/ParseQRCode.mjs";
const testStatus = {
allTestsPassing: true,

View File

@ -0,0 +1,35 @@
/**
* ParseQRCode API tests.
*
* @author Sanjays2402
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
import OperationConfig from "../../../src/core/config/OperationConfig.json" with { type: "json" };
import it from "../assertionHandler.mjs";
import assert from "assert";
TestRegister.addApiTests([
/*
* Regression test for #2610.
*
* Parse QR Code used to declare a `checks` regex that matched any JPEG,
* PNG, GIF, WEBP or BMP magic bytes. Magic aggregates every operation
* with a `checks` property, so any image input ran through a full QR
* parse attempt, which in turn emitted a "Could not read a QR code from
* the image" warning to the browser console for every image. There is
* no cheap way to detect a QR code without attempting a full parse, so
* Parse QR Code must not participate in Magic; users can add it
* manually when they know an image contains a QR code.
*/
it("Parse QR Code: must not participate in Magic (#2610)", () => {
const op = OperationConfig["Parse QR Code"];
assert(op, "Parse QR Code operation is missing from OperationConfig");
assert(
!op.checks || op.checks.length === 0,
"Parse QR Code must not declare `checks`; otherwise Magic will run a " +
"QR parse on every image and spam the console (see issue #2610)."
);
}),
]);

View File

@ -150,5 +150,38 @@ TestRegister.addTests([
args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, ""]
}
]
},
{
name: "Automated Validation: Valid Arg Selector value",
input: "test",
expectedOutput: "Success",
recipeConfig: [
{
op: "Automated Validation Test Op",
args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", "Option 2"]
}
]
},
{
name: "Automated Validation: Invalid Arg Selector value",
input: "test",
expectedOutput: "Arg Selector Ingredient must be one of the following: Option 1, Option 2.",
recipeConfig: [
{
op: "Automated Validation Test Op",
args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", "Option 3"]
}
]
},
{
name: "Automated Validation: Arg Selector value empty (invalid)",
input: "test",
expectedOutput: "Arg Selector Ingredient cannot be empty.",
recipeConfig: [
{
op: "Automated Validation Test Op",
args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }, "Option 1", ""]
}
]
}
]);

View File

@ -152,5 +152,49 @@ TestRegister.addTests([
args: [1, false, false]
}
]
},
{
name: "Magic: invalid zero A1Z26 values do not match",
input: "0,0",
unexpectedMatch: /A1Z26 Cipher Decode/,
recipeConfig: [
{
op: "Magic",
args: [0, false, false]
}
]
},
{
name: "Magic: invalid high A1Z26 values do not match",
input: "27,1",
unexpectedMatch: /A1Z26 Cipher Decode/,
recipeConfig: [
{
op: "Magic",
args: [0, false, false]
}
]
},
{
name: "Magic: valid leading-zero A1Z26 values match",
input: "26,01",
expectedMatch: /A1Z26 Cipher Decode/,
recipeConfig: [
{
op: "Magic",
args: [0, false, false]
}
]
},
{
name: "Magic: valid leading-zero A1Z26 values match with two digits",
input: "09,10",
expectedMatch: /A1Z26 Cipher Decode/,
recipeConfig: [
{
op: "Magic",
args: [0, false, false]
}
]
}
]);

View File

@ -0,0 +1,137 @@
/**
* Modular Exponentiation tests.
*
* @author p-leriche [philip.leriche@cantab.net]
*
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Modular Exponentiation: basic example (2^10 mod 1000)",
input: "",
expectedOutput: "24",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["2", "1000", "10"],
},
],
},
{
name: "Modular Exponentiation: small values (3^5 mod 7)",
input: "",
expectedOutput: "5",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["3", "7", "5"],
},
],
},
{
name: "Modular Exponentiation: exponent zero",
input: "",
expectedOutput: "1",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["999", "100", "0"],
},
],
},
{
name: "Modular Exponentiation: base one",
input: "",
expectedOutput: "1",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["1", "1000", "999"],
},
],
},
{
name: "Modular Exponentiation: hexadecimal input",
input: "",
expectedOutput: "256",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["0x10", "1000", "0x2"],
},
],
},
{
name: "Modular Exponentiation: using input field for base",
input: "5",
expectedOutput: "6",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["", "7", "3"],
},
],
},
{
name: "Modular Exponentiation: using input field for exponent",
input: "4",
expectedOutput: "5",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["2", "11", ""],
},
],
},
{
name: "Modular Exponentiation: RSA-like example (small)",
input: "",
expectedOutput: "561",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["123", "1000", "456"],
},
],
},
{
name: "Modular Exponentiation: large base and exponent",
input: "",
expectedOutput: "560583526",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["123456789", "1000000007", "65537"],
},
],
},
{
name: "Modular Exponentiation: crypto-sized numbers (RSA-2048 simulation)",
input: "",
expectedOutput: "1",
recipeConfig: [
{
op: "Modular Exponentiation",
args: [
"12345678901234567890123456789012345678901234567890",
"99999999999999999999999999999999999999999999999999",
"0"
],
},
],
},
{
name: "Modular Exponentiation: Fermat's Little Theorem (a^(p-1) mod p = 1)",
input: "",
expectedOutput: "1",
recipeConfig: [
{
op: "Modular Exponentiation",
args: ["3", "11", "10"],
},
],
},
]);