-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Expand file tree
/
Copy pathapi.mjs
More file actions
350 lines (305 loc) · 11.1 KB
/
api.mjs
File metadata and controls
350 lines (305 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/**
* 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" with { type: "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[arg.defaultIndex ?? 0];
}
if (arg.type === "editableOption") {
return typeof arg.value === "string" ? arg.value : arg.value[arg.defaultIndex ?? 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";
const isFlowControl = opInstance.flowControl;
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);
// SPECIAL CASE for Magic. Other flowControl operations will
// not work because the opList is not passed in.
if (isFlowControl) {
opInstance.ingValues = transformedArgs;
const state = {
progress: 0,
dish: ensureIsDish(transformedInput),
opList: [opInstance],
};
const updatedState = await opInstance.run(state);
return new NodeDish({
value: updatedState.dish.value,
type: opInstance.outputType,
});
}
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);
// Used in NodeRecipe to check for flowControl ops
wrapped.flowControl = isFlowControl;
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
*
* @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 {NodeDish} of the result
* @throws {TypeError} if invalid recipe given.
*/
export async function bake(input, recipeConfig) {
const recipe = new NodeRecipe(recipeConfig);
const dish = ensureIsDish(input);
return await 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;
}