create recipeState and hydrate recipeconfig before recipe is constructed

This commit is contained in:
d98762625 2020-03-20 13:06:29 +00:00
parent 536a82684c
commit d491a7f0fc
5 changed files with 127 additions and 159 deletions

View file

@ -38,9 +38,8 @@ class Chef {
* @returns {number} response.error - The error object thrown by a failed operation (false if no error) * @returns {number} response.error - The error object thrown by a failed operation (false if no error)
*/ */
async bake(input, recipeConfig, options) { async bake(input, recipeConfig, options) {
log.debug("Chef baking");
const startTime = new Date().getTime(), const startTime = new Date().getTime(),
recipe = new Recipe(recipeConfig), recipe = await Recipe.buildRecipe(recipeConfig),
containsFc = recipe.containsFlowControl(), containsFc = recipe.containsFlowControl(),
notUTF8 = options && "treatAsUtf8" in options && !options.treatAsUtf8; notUTF8 = options && "treatAsUtf8" in options && !options.treatAsUtf8;
let error = false, let error = false,
@ -107,11 +106,11 @@ class Chef {
* @param {Object[]} recipeConfig - The recipe configuration object * @param {Object[]} recipeConfig - The recipe configuration object
* @returns {number} The time it took to run the silent bake in milliseconds. * @returns {number} The time it took to run the silent bake in milliseconds.
*/ */
silentBake(recipeConfig) { async silentBake(recipeConfig) {
log.debug("Running silent bake"); log.debug("Running silent bake");
const startTime = new Date().getTime(), const startTime = new Date().getTime(),
recipe = new Recipe(recipeConfig), recipe = await Recipe.buildRecipe(recipeConfig),
dish = new Dish(); dish = new Dish();
try { try {
@ -134,8 +133,8 @@ class Chef {
* @returns {Object} * @returns {Object}
*/ */
async calculateHighlights(recipeConfig, direction, pos) { async calculateHighlights(recipeConfig, direction, pos) {
const recipe = new Recipe(recipeConfig); const recipe = await Recipe.buildRecipe(recipeConfig);
const highlights = await recipe.generateHighlightList(); const highlights = recipe.generateHighlightList();
if (!highlights) return false; if (!highlights) return false;

View file

@ -24,61 +24,14 @@ class Recipe {
* *
* @param {Object} recipeConfig * @param {Object} recipeConfig
*/ */
constructor(recipeConfig) { constructor(operations=[]) {
this.opList = []; this.state = new RecipeState();
this.state.opList = operations;
if (recipeConfig) {
this._parseConfig(recipeConfig);
}
} }
/**
* Reads and parses the given config.
*
* @private
* @param {Object} recipeConfig
*/
_parseConfig(recipeConfig) {
recipeConfig.forEach(c => {
this.opList.push({
name: c.op,
module: OperationConfig[c.op].module,
ingValues: c.args,
breakpoint: c.breakpoint,
disabled: c.disabled,
});
});
}
/**
* Populate elements of opList with operation instances.
* Dynamic import here removes top-level cyclic dependency issue.
*
* @private
*/
async _hydrateOpList() {
if (!modules) {
// Using Webpack Magic Comments to force the dynamic import to be included in the main chunk
// https://webpack.js.org/api/module-methods/
modules = await import(/* webpackMode: "eager" */ "./config/modules/OpModules.mjs");
modules = modules.default;
}
this.opList = this.opList.map(o => {
if (o instanceof Operation) {
return o;
} else {
const op = new modules[o.module][o.name]();
op.ingValues = o.ingValues;
op.breakpoint = o.breakpoint;
op.disabled = o.disabled;
return op;
}
});
}
/** /**
* Returns the value of the Recipe as it should be displayed in a recipe config. * Returns the value of the Recipe as it should be displayed in a recipe config.
@ -86,12 +39,15 @@ class Recipe {
* @returns {Object[]} * @returns {Object[]}
*/ */
get config() { get config() {
return this.opList.map(op => ({ return this.state.opList.map(op => ({
op: op.name, op: op.name,
args: op.ingValues, args: op.ingValues,
})); }));
} }
get opList() {
return this.state.opList;
}
/** /**
* Adds a new Operation to this Recipe. * Adds a new Operation to this Recipe.
@ -99,7 +55,7 @@ class Recipe {
* @param {Operation} operation * @param {Operation} operation
*/ */
addOperation(operation) { addOperation(operation) {
this.opList.push(operation); this.state.addOperation(operation);
} }
@ -110,58 +66,18 @@ class Recipe {
*/ */
addOperations(operations) { addOperations(operations) {
operations.forEach(o => { operations.forEach(o => {
if (o instanceof Operation) { this.state.addOperation(o);
this.opList.push(o);
} else {
this.opList.push({
name: o.name,
module: o.module,
ingValues: o.args,
breakpoint: o.breakpoint,
disabled: o.disabled,
});
}
}); });
} }
/**
* Set a breakpoint on a specified Operation.
*
* @param {number} position - The index of the Operation
* @param {boolean} value
*/
setBreakpoint(position, value) {
try {
this.opList[position].breakpoint = value;
} catch (err) {
// Ignore index error
}
}
/**
* Remove breakpoints on all Operations in the Recipe up to the specified position. Used by Flow
* Control Fork operation.
*
* @param {number} pos
*/
removeBreaksUpTo(pos) {
for (let i = 0; i < pos; i++) {
this.opList[i].breakpoint = false;
}
}
/** /**
* Returns true if there is a Flow Control Operation in this Recipe. * Returns true if there is a Flow Control Operation in this Recipe.
* *
* @returns {boolean} * @returns {boolean}
*/ */
containsFlowControl() { containsFlowControl() {
return this.opList.reduce((acc, curr) => { return this.state.containsFlowControl()
return acc || curr.flowControl;
}, false);
} }
@ -175,18 +91,16 @@ class Recipe {
* - The final progress through the recipe * - The final progress through the recipe
*/ */
async execute(dish, forkState={}) { async execute(dish, forkState={}) {
let op, input, output, let op, input, output;
numJumps = 0, this.state.dish = dish;
numRegisters = forkState.numRegisters || 0; this.state.updateForkState(forkState);
this.lastRunOp = null; this.lastRunOp = null;
await this._hydrateOpList(); log.debug(`[*] Executing recipe of ${this.state.opList.length} operations`);
log.debug(`[*] Executing recipe of ${this.opList.length} operations`); while (this.state.progress < this.state.opList.length) {
const i = this.state.progress;
for (let i = 0; i < this.opList.length; i++) { op = this.state.currentOp;
op = this.opList[i];
log.debug(`[${i}] ${op.name} ${JSON.stringify(op.ingValues)}`); log.debug(`[${i}] ${op.name} ${JSON.stringify(op.ingValues)}`);
if (op.disabled) { if (op.disabled) {
log.debug("Operation is disabled, skipping"); log.debug("Operation is disabled, skipping");
@ -198,32 +112,24 @@ class Recipe {
} }
try { try {
input = await dish.get(op.inputType); input = await this.state.dish.get(op.inputType);
log.debug(`Executing operation '${op.name}'`); log.debug(`Executing operation '${op.name}'`);
if (isWorkerEnvironment()) { if (isWorkerEnvironment()) {
self.sendStatusMessage(`Baking... (${i+1}/${this.opList.length})`); self.sendStatusMessage(`Baking... (${i+1}/${this.state.opList.length})`);
self.sendProgressMessage(i + 1, this.opList.length); self.sendProgressMessage(i + 1, this.state.opList.length);
} }
if (op.flowControl) { if (op.flowControl) {
// Package up the current state
let state = {
"progress": i,
"dish": dish,
"opList": this.opList,
"numJumps": numJumps,
"numRegisters": numRegisters,
"forkOffset": forkState.forkOffset || 0
};
state = await op.run(state); this.state = await op.run(this.state);
i = state.progress;
numJumps = state.numJumps;
numRegisters = state.numRegisters; this.state.progress++;
} else { } else {
output = await op.run(input, op.ingValues); output = await op.run(input, op.ingValues);
dish.set(output, op.outputType); this.state.dish.set(output, op.outputType);
this.state.progress++;
} }
this.lastRunOp = op; this.lastRunOp = op;
} catch (err) { } catch (err) {
@ -232,11 +138,11 @@ class Recipe {
(err.type && err.type === "OperationError")) { (err.type && err.type === "OperationError")) {
// Cannot rely on `err instanceof OperationError` here as extending // Cannot rely on `err instanceof OperationError` here as extending
// native types is not fully supported yet. // native types is not fully supported yet.
dish.set(err.message, "string"); this.state.dish.set(err.message, "string");
return i; return i;
} else if (err instanceof DishError || } else if (err instanceof DishError ||
(err.type && err.type === "DishError")) { (err.type && err.type === "DishError")) {
dish.set(err.message, "string"); this.state.dish.set(err.message, "string");
return i; return i;
} else { } else {
const e = typeof err == "string" ? { message: err } : err; const e = typeof err == "string" ? { message: err } : err;
@ -255,7 +161,7 @@ class Recipe {
} }
log.debug("Recipe complete"); log.debug("Recipe complete");
return this.opList.length; return this.state.opList.length;
} }
@ -284,18 +190,6 @@ class Recipe {
return JSON.stringify(this.config); return JSON.stringify(this.config);
} }
/**
* Creates a Recipe from a given configuration string.
*
* @param {string} recipeStr
*/
fromString(recipeStr) {
const recipeConfig = JSON.parse(recipeStr);
this._parseConfig(recipeConfig);
}
/** /**
* Generates a list of all the highlight functions assigned to operations in the recipe, if the * Generates a list of all the highlight functions assigned to operations in the recipe, if the
* entire recipe supports highlighting. * entire recipe supports highlighting.
@ -305,13 +199,15 @@ class Recipe {
* @returns {function} highlights[].b * @returns {function} highlights[].b
* @returns {Object[]} highlights[].args * @returns {Object[]} highlights[].args
*/ */
async generateHighlightList() { generateHighlightList() {
await this._hydrateOpList();
const highlights = []; const highlights = [];
while (this.state.progress < this.state.opList.length) {
for (let i = 0; i < this.opList.length; i++) { // for (let i = 0; i < this.state.opList.length; i++) {
const op = this.opList[i]; const op = this.state.currentOp;
if (op.disabled) continue; if (op.disabled) {
this.state.progress++;
continue;
}
// If any breakpoints are set, do not attempt to highlight // If any breakpoints are set, do not attempt to highlight
if (op.breakpoint) return false; if (op.breakpoint) return false;
@ -329,18 +225,92 @@ class Recipe {
return highlights; return highlights;
} }
/** /**
* Determines whether the previous operation has a different presentation type to its normal output. *
* * @param recipeConfig
* @param {number} progress
* @returns {boolean}
*/ */
lastOpPresented(progress) { static async buildRecipe(recipeConfig) {
if (progress < 1) return false; let operations = [];
return this.opList[progress-1].presentType !== this.opList[progress-1].outputType; recipeConfig.forEach(c => {
operations.push({
name: c.op,
module: OperationConfig[c.op].module,
ingValues: c.args,
breakpoint: c.breakpoint,
disabled: c.disabled,
});
});
if (!modules) {
// Using Webpack Magic Comments to force the dynamic import to be included in the main chunk
// https://webpack.js.org/api/module-methods/
modules = await import(/* webpackMode: "eager" */ "./config/modules/OpModules.mjs");
modules = modules.default;
}
const hydratedOperations = operations.map(o => {
if (o instanceof Operation) {
return o;
} else {
const op = new modules[o.module][o.name]();
op.ingValues = o.ingValues;
op.breakpoint = o.breakpoint;
op.disabled = o.disabled;
return op;
}
});
return new Recipe(hydratedOperations);
} }
}
class RecipeState {
constructor() {
this.dish = null;
this.opList = [];
this.progress = 0;
this.forkOffset = 0;
this.numRegisters = 0;
this.numJumps = 0;
}
get currentOp() {
return this.opList[this.progress];
}
addOperation(operation) {
this.opList.push(operation);
}
containsFlowControl() {
return this.opList.reduce((p, c) => {
return p || c.flowControl;
}, false);
}
updateForkState(forkState) {
if (forkState.progress || forkState.progress == 0) {
this.progress = forkState.progress
}
if (forkState.numRegisters) {
this.numRegisters = forkState.numRegisters;
}
if (forkState.numJumps) {
this.numJumps = forkState.numJumps;
}
if (forkState.forkOffset) {
this.forkOffset = forkState.forkOffset;
}
}
} }
export default Recipe; export default Recipe;
export { RecipeState };

View file

@ -398,7 +398,7 @@ class Magic {
if (isWorkerEnvironment()) self.loadRequiredModules(recipeConfig); if (isWorkerEnvironment()) self.loadRequiredModules(recipeConfig);
const recipe = new Recipe(recipeConfig); const recipe = await Recipe.buildRecipe(recipeConfig);
try { try {
await recipe.execute(dish); await recipe.execute(dish);
// Return an empty buffer if the recipe did not run to completion // Return an empty buffer if the recipe did not run to completion

View file

@ -112,9 +112,9 @@ class Magic extends Operation {
options.forEach(option => { options.forEach(option => {
// Construct recipe URL // Construct recipe URL
// Replace this Magic op with the generated recipe // Replace this Magic op with the generated recipe
const recipeConfig = currentRecipeConfig.slice(0, this.state.progress) const recipeConfig = currentRecipeConfig.slice(0, this.state.progress - 1)
.concat(option.recipe) .concat(option.recipe)
.concat(currentRecipeConfig.slice(this.state.progress + 1)), .concat(currentRecipeConfig.slice(this.state.progress)),
recipeURL = "recipe=" + Utils.encodeURIFragment(Utils.generatePrettyRecipe(recipeConfig)); recipeURL = "recipe=" + Utils.encodeURIFragment(Utils.generatePrettyRecipe(recipeConfig));
let language = "", let language = "",

View file

@ -79,7 +79,6 @@ import "./tests/StrUtils.mjs";
import "./tests/SymmetricDifference.mjs"; import "./tests/SymmetricDifference.mjs";
import "./tests/TextEncodingBruteForce.mjs"; import "./tests/TextEncodingBruteForce.mjs";
import "./tests/TranslateDateTimeFormat.mjs"; import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Magic.mjs";
import "./tests/ParseTLV.mjs"; import "./tests/ParseTLV.mjs";
import "./tests/Media.mjs"; import "./tests/Media.mjs";
import "./tests/ToFromInsensitiveRegex.mjs"; import "./tests/ToFromInsensitiveRegex.mjs";