123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- /**
- * Wrap operations for consumption in Node.
- *
- * @author d98762625 [d98762625@gmail.com]
- * @copyright Crown Copyright 2018
- * @license Apache-2.0
- */
- /* eslint no-console: ["off"] */
- import NodeDish from "./NodeDish.mjs";
- import NodeRecipe from "./NodeRecipe.mjs";
- import OperationConfig from "../core/config/OperationConfig.json";
- import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
- import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";
- /**
- * transformArgs
- *
- * Take the default args array and update with any user-defined
- * operation arguments. Allows user to define arguments in object style,
- * with accommodating name matching. Using named args in the API is more
- * clear to the user.
- *
- * Argument name matching is case and space insensitive
- * @private
- * @param {Object[]} originalArgs - the operation-s args list
- * @param {Object} newArgs - any inputted args
- */
- function transformArgs(opArgsList, newArgs) {
- if (newArgs && Array.isArray(newArgs)) {
- return newArgs;
- }
- // Filter out arg values that are list subheadings - they are surrounded in [].
- // See Strings op for example.
- const opArgs = Object.assign([], opArgsList).map((a) => {
- if (Array.isArray(a.value)) {
- a.value = removeSubheadingsFromArray(a.value);
- }
- return a;
- });
- // Reconcile object style arg info to fit operation args shape.
- if (newArgs) {
- Object.keys(newArgs).map((key) => {
- const index = opArgs.findIndex((arg) => {
- return arg.name.toLowerCase().replace(/ /g, "") ===
- key.toLowerCase().replace(/ /g, "");
- });
- if (index > -1) {
- const argument = opArgs[index];
- if (argument.type === "toggleString") {
- if (typeof newArgs[key] === "string") {
- argument.string = newArgs[key];
- } else {
- argument.string = newArgs[key].string;
- argument.option = newArgs[key].option;
- }
- } else if (argument.type === "editableOption") {
- // takes key: "option", key: {name, val: "string"}, key: {name, val: [...]}
- argument.value = typeof newArgs[key] === "string" ? newArgs[key]: newArgs[key].value;
- } else {
- argument.value = newArgs[key];
- }
- }
- });
- }
- // Sanitise args
- return opArgs.map((arg) => {
- if (arg.type === "option") {
- // pick default option if not already chosen
- return typeof arg.value === "string" ? arg.value : arg.value[0];
- }
- if (arg.type === "editableOption") {
- return typeof arg.value === "string" ? arg.value : arg.value[0].value;
- }
- if (arg.type === "toggleString") {
- // ensure string and option exist when user hasn't defined
- arg.string = arg.string || "";
- arg.option = arg.option || arg.toggleValues[0];
- return arg;
- }
- return arg.value;
- });
- }
- /**
- * Ensure an input is a SyncDish object.
- * @param input
- */
- function ensureIsDish(input) {
- if (!input) {
- return new NodeDish();
- }
- if (input instanceof NodeDish) {
- return input;
- } else {
- return new NodeDish(input);
- }
- }
- /**
- * prepareOp: transform args, make input the right type.
- * Also convert any Buffers to ArrayBuffers.
- * @param opInstance - instance of the operation
- * @param input - operation input
- * @param args - operation args
- */
- function prepareOp(opInstance, input, args) {
- const dish = ensureIsDish(input);
- // Transform object-style args to original args array
- const transformedArgs = transformArgs(opInstance.args, args);
- const transformedInput = dish.get(opInstance.inputType);
- return {transformedInput, transformedArgs};
- }
- /**
- * createArgInfo
- *
- * Create an object of options for each argument in the given operation
- *
- * Argument names are converted to camel case for consistency.
- *
- * @param {Operation} op - the operation to extract args from
- * @returns {{}} - arrays of options for args.
- */
- function createArgInfo(op) {
- const result = {};
- op.args.forEach((a) => {
- if (a.type === "option" || a.type === "editableOption") {
- result[sentenceToCamelCase(a.name)] = {
- type: a.type,
- options: removeSubheadingsFromArray(a.value)
- };
- } else if (a.type === "toggleString") {
- result[sentenceToCamelCase(a.name)] = {
- type: a.type,
- value: a.value,
- toggleValues: removeSubheadingsFromArray(a.toggleValues),
- };
- } else {
- result[sentenceToCamelCase(a.name)] = {
- type: a.type,
- value: a.value,
- };
- }
- });
- return result;
- }
- /**
- * Wrap an operation to be consumed by node API.
- * Checks to see if run function is async or not.
- * new Operation().run() becomes operation()
- * Perform type conversion on input
- * @private
- * @param {Operation} Operation
- * @returns {Function} The operation's run function, wrapped in
- * some type conversion logic
- */
- export function _wrap(OpClass) {
- // Check to see if class's run function is async.
- const opInstance = new OpClass();
- const isAsync = opInstance.run.constructor.name === "AsyncFunction";
- let wrapped;
- // If async, wrap must be async.
- if (isAsync) {
- /**
- * Async wrapped operation run function
- * @param {*} input
- * @param {Object | String[]} args - either in Object or normal args array
- * @returns {Promise<SyncDish>} operation's output, on a Dish.
- * @throws {OperationError} if the operation throws one.
- */
- wrapped = async (input, args=null) => {
- const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
- const result = await opInstance.run(transformedInput, transformedArgs);
- return new NodeDish({
- value: result,
- type: opInstance.outputType,
- });
- };
- } else {
- /**
- * wrapped operation run function
- * @param {*} input
- * @param {Object | String[]} args - either in Object or normal args array
- * @returns {SyncDish} operation's output, on a Dish.
- * @throws {OperationError} if the operation throws one.
- */
- wrapped = (input, args=null) => {
- const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
- const result = opInstance.run(transformedInput, transformedArgs);
- return new NodeDish({
- value: result,
- type: opInstance.outputType,
- });
- };
- }
- // used in chef.help
- wrapped.opName = OpClass.name;
- wrapped.args = createArgInfo(opInstance);
- return wrapped;
- }
- /**
- * help: Give information about operations matching the given search term,
- * or inputted operation.
- *
- * @param {String || wrapped operation} input - the name of the operation to get help for.
- * Case and whitespace are ignored in search.
- * @returns {Object[]} Config of matching operations.
- */
- export function help(input) {
- let searchTerm = false;
- if (typeof input === "string") {
- searchTerm = input;
- } else if (typeof input === "function") {
- searchTerm = input.opName;
- }
- if (!searchTerm) {
- return null;
- }
- let exactMatchExists = false;
- // Look for matches in operation name and description, listing name
- // matches first.
- const matches = Object.keys(OperationConfig)
- // hydrate operation: swap op name for op config object (with name)
- .map((m) => {
- const hydrated = OperationConfig[m];
- hydrated.name = m;
- // flag up an exact name match. Only first exact match counts.
- if (!exactMatchExists) {
- exactMatchExists = sanitise(hydrated.name) === sanitise(searchTerm);
- }
- // Return hydrated along with what type of match it was
- return {
- hydrated,
- nameExactMatch: sanitise(hydrated.name) === sanitise(searchTerm),
- nameMatch: sanitise(hydrated.name).includes(sanitise(searchTerm)),
- descMatch: sanitise(hydrated.description).includes(sanitise(searchTerm))
- };
- })
- // Filter out non-matches. If exact match exists, filter out all others.
- .filter((result) => {
- if (exactMatchExists) {
- return !!result.nameExactMatch;
- }
- return result.nameMatch || result.descMatch;
- })
- // sort results with name match first
- .sort((a, b) => {
- const aInt = a.nameMatch ? 1 : 0;
- const bInt = b.nameMatch ? 1 : 0;
- return bInt - aInt;
- })
- // extract just the hydrated config
- .map(result => result.hydrated);
- if (matches && matches.length) {
- // console.log(`${matches.length} result${matches.length > 1 ? "s" : ""} found.`);
- return matches;
- }
- // console.log("No results found.");
- return null;
- }
- /**
- * bake [Wrapped] - Perform an array of operations on some input.
- * @returns {Function}
- */
- export function bake() {
- /**
- * bake
- *
- * @param {*} input - some input for a recipe.
- * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig -
- * An operation, operation name, or an array of either.
- * @returns {SyncDish} of the result
- * @throws {TypeError} if invalid recipe given.
- */
- return function(input, recipeConfig) {
- const recipe = new NodeRecipe(recipeConfig);
- const dish = ensureIsDish(input);
- return recipe.execute(dish);
- };
- }
- /**
- * explainExcludedFunction
- *
- * Explain that the given operation is not included in the Node.js version.
- * @param {String} name - name of operation
- */
- export function _explainExcludedFunction(name) {
- /**
- * Throw new error type with useful message.
- */
- const func = () => {
- throw new ExcludedOperationError(`Sorry, the ${name} operation is not available in the Node.js version of CyberChef.`);
- };
- // Add opName prop so NodeRecipe can handle it, just like wrap does.
- func.opName = name;
- return func;
- }
|