Merge branch 'master' into she-key-update
This commit is contained in:
commit
353890fd82
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -247,6 +247,7 @@
|
||||
"Divide",
|
||||
"MOD",
|
||||
"Extended GCD",
|
||||
"Modular Exponentiation",
|
||||
"Modular Inverse",
|
||||
"Mean",
|
||||
"Median",
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
111
src/core/operations/ModularExponentiation.mjs
Normal file
111
src/core/operations/ModularExponentiation.mjs
Normal 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;
|
||||
@ -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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
35
tests/node/tests/ParseQRCode.mjs
Normal file
35
tests/node/tests/ParseQRCode.mjs
Normal 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)."
|
||||
);
|
||||
}),
|
||||
]);
|
||||
@ -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", ""]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
@ -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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
137
tests/operations/tests/ModularExponentiation.mjs
Normal file
137
tests/operations/tests/ModularExponentiation.mjs
Normal 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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user