api.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /**
  2. * Wrap operations for consumption in Node.
  3. *
  4. * @author d98762625 [d98762625@gmail.com]
  5. * @copyright Crown Copyright 2018
  6. * @license Apache-2.0
  7. */
  8. import Dish from "../core/Dish";
  9. import SyncDish from "./SyncDish";
  10. import Recipe from "./Recipe";
  11. import OperationConfig from "./config/OperationConfig.json";
  12. import { sanitise } from "./apiUtils";
  13. import ExludedOperationError from "../core/errors/ExcludedOperationError";
  14. /**
  15. * Extract default arg value from operation argument
  16. * @param {Object} arg - an arg from an operation
  17. */
  18. function extractArg(arg) {
  19. if (arg.type === "option") {
  20. // pick default option if not already chosen
  21. return typeof arg.value === "string" ? arg.value : arg.value[0];
  22. }
  23. if (arg.type === "editableOption") {
  24. return typeof arg.value === "string" ? arg.value : arg.value[0].value;
  25. }
  26. if (arg.type === "toggleString") {
  27. // ensure string and option exist when user hasn't defined
  28. arg.string = arg.string || "";
  29. arg.option = arg.option || arg.toggleValues[0];
  30. return arg;
  31. }
  32. return arg.value;
  33. }
  34. /**
  35. * transformArgs
  36. *
  37. * Take the default args array and update with any user-defined
  38. * operation arguments. Allows user to define arguments in object style,
  39. * with accommodating name matching. Using named args in the API is more
  40. * clear to the user.
  41. *
  42. * Argument name matching is case and space insensitive
  43. * @private
  44. * @param {Object[]} originalArgs - the operation-s args list
  45. * @param {Object} newArgs - any inputted args
  46. */
  47. function transformArgs(originalArgs, newArgs) {
  48. // Filter out arg values that are list subheadings - they are surrounded in [].
  49. // See Strings op for example.
  50. const allArgs = Object.assign([], originalArgs).map((a) => {
  51. if (Array.isArray(a.value)) {
  52. a.value = a.value.filter((v) => {
  53. if (typeof v === "string") {
  54. return !v.match(/^\[[\s\S]*\]$/); // Matches anything surrounded in [ ]
  55. }
  56. return true;
  57. });
  58. }
  59. return a;
  60. });
  61. if (newArgs) {
  62. Object.keys(newArgs).map((key) => {
  63. const index = allArgs.findIndex((arg) => {
  64. return arg.name.toLowerCase().replace(/ /g, "") ===
  65. key.toLowerCase().replace(/ /g, "");
  66. });
  67. if (index > -1) {
  68. const argument = allArgs[index];
  69. if (argument.type === "toggleString") {
  70. if (typeof newArgs[key] === "string") {
  71. argument.string = newArgs[key];
  72. } else {
  73. argument.string = newArgs[key].string;
  74. argument.option = newArgs[key].option;
  75. }
  76. } else if (argument.type === "editableOption") {
  77. // takes key: "option", key: {name, val: "string"}, key: {name, val: [...]}
  78. argument.value = typeof newArgs[key] === "string" ? newArgs[key]: newArgs[key].value;
  79. } else {
  80. argument.value = newArgs[key];
  81. }
  82. }
  83. });
  84. }
  85. return allArgs.map(extractArg);
  86. }
  87. /**
  88. * Ensure an input is a SyncDish object.
  89. * @param input
  90. */
  91. function ensureIsDish(input) {
  92. if (!input) {
  93. return new SyncDish();
  94. }
  95. let dish;
  96. if (input instanceof SyncDish) {
  97. dish = input;
  98. } else {
  99. dish = new SyncDish();
  100. const type = Dish.typeEnum(input.constructor.name);
  101. dish.set(input, type);
  102. }
  103. return dish;
  104. }
  105. /**
  106. * prepareOp: transform args, make input the right type.
  107. * Also convert any Buffers to ArrayBuffers.
  108. * @param opInstance - instance of the operation
  109. * @param input - operation input
  110. * @param args - operation args
  111. */
  112. function prepareOp(opInstance, input, args) {
  113. // convert any Buffers into ArrayBuffers.
  114. if (input instanceof Buffer) {
  115. input = input.buffer;
  116. }
  117. const dish = ensureIsDish(input);
  118. let transformedArgs;
  119. // Transform object-style args to original args array
  120. if (!Array.isArray(args)) {
  121. transformedArgs = transformArgs(opInstance.args, args);
  122. } else {
  123. transformedArgs = args;
  124. }
  125. const transformedInput = dish.get(opInstance.inputType);
  126. return {transformedInput, transformedArgs};
  127. }
  128. /**
  129. * Wrap an operation to be consumed by node API.
  130. * Checks to see if run function is async or not.
  131. * new Operation().run() becomes operation()
  132. * Perform type conversion on input
  133. * @private
  134. * @param {Operation} Operation
  135. * @returns {Function} The operation's run function, wrapped in
  136. * some type conversion logic
  137. */
  138. export function wrap(OpClass) {
  139. // Check to see if class's run function is async.
  140. const opInstance = new OpClass();
  141. const isAsync = opInstance.run.constructor.name === "AsyncFunction";
  142. let wrapped;
  143. // If async, wrap must be async.
  144. if (isAsync) {
  145. /**
  146. * Async wrapped operation run function
  147. * @param {*} input
  148. * @param {Object | String[]} args - either in Object or normal args array
  149. * @returns {Promise<SyncDish>} operation's output, on a Dish.
  150. * @throws {OperationError} if the operation throws one.
  151. */
  152. wrapped = async (input, args=null) => {
  153. const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
  154. const result = await opInstance.run(transformedInput, transformedArgs);
  155. return new SyncDish({
  156. value: result,
  157. type: opInstance.outputType
  158. });
  159. };
  160. } else {
  161. /**
  162. * wrapped operation run function
  163. * @param {*} input
  164. * @param {Object | String[]} args - either in Object or normal args array
  165. * @returns {SyncDish} operation's output, on a Dish.
  166. * @throws {OperationError} if the operation throws one.
  167. */
  168. wrapped = (input, args=null) => {
  169. const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args);
  170. const result = opInstance.run(transformedInput, transformedArgs);
  171. return new SyncDish({
  172. value: result,
  173. type: opInstance.outputType
  174. });
  175. };
  176. }
  177. // used in chef.help
  178. wrapped.opName = OpClass.name;
  179. return wrapped;
  180. }
  181. /**
  182. * @namespace Api
  183. * @param {String} searchTerm - the name of the operation to get help for.
  184. * Case and whitespace are ignored in search.
  185. * @returns {Object} Describe function matching searchTerm.
  186. */
  187. export function help(searchTerm) {
  188. let sanitised = false;
  189. if (typeof searchTerm === "string") {
  190. sanitised = searchTerm;
  191. } else if (typeof searchTerm === "function") {
  192. sanitised = searchTerm.opName;
  193. }
  194. if (!sanitised) {
  195. return null;
  196. }
  197. const key = Object.keys(OperationConfig)
  198. .find(o => sanitise(o) === sanitise(sanitised));
  199. if (key) {
  200. const result = OperationConfig[key];
  201. result.name = key;
  202. return result;
  203. }
  204. return null;
  205. }
  206. /**
  207. * bake [Wrapped] - Perform an array of operations on some input.
  208. * @param operations array of chef's operations (used in wrapping stage)
  209. * @returns {Function}
  210. */
  211. export function bake(operations){
  212. /**
  213. * bake
  214. *
  215. * @param {*} input - some input for a recipe.
  216. * @param {String | Function | String[] | Function[] | [String | Function]} recipeConfig -
  217. * An operation, operation name, or an array of either.
  218. * @returns {SyncDish} of the result
  219. * @throws {TypeError} if invalid recipe given.
  220. */
  221. return function(input, recipeConfig) {
  222. const recipe = new Recipe(recipeConfig);
  223. const dish = ensureIsDish(input);
  224. return recipe.execute(dish);
  225. };
  226. }
  227. /**
  228. * Explain that the given operation is not included in the Node.js version.
  229. * @param {String} name - name of operation
  230. */
  231. export function explainExludedFunction(name) {
  232. /**
  233. * Throw new error type with useful message.
  234. */
  235. const func = () => {
  236. throw new ExludedOperationError(`Sorry, the ${name} operation is not available in the Node.js version of CyberChef.`);
  237. };
  238. // Add opName prop so Recipe can handle it, just like wrap does.
  239. func.opName = name;
  240. return func;
  241. }