Merge branch 'multiple-input-files' of https://github.com/j433866/CyberChef into j433866-multiple-input-files

This commit is contained in:
n1474335 2019-07-04 13:52:26 +01:00
commit e49974beaa
45 changed files with 6643 additions and 1342 deletions

View file

@ -28,8 +28,6 @@ class Chef {
* @param {Object[]} recipeConfig - The recipe configuration object
* @param {Object} options - The options object storing various user choices
* @param {boolean} options.attempHighlight - Whether or not to attempt highlighting
* @param {number} progress - The position in the recipe to start from
* @param {number} [step] - Whether to only execute one operation in the recipe
*
* @returns {Object} response
* @returns {string} response.result - The output of the recipe
@ -38,46 +36,20 @@ class Chef {
* @returns {number} response.duration - The number of ms it took to execute the recipe
* @returns {number} response.error - The error object thrown by a failed operation (false if no error)
*/
async bake(input, recipeConfig, options, progress, step) {
async bake(input, recipeConfig, options) {
log.debug("Chef baking");
const startTime = new Date().getTime(),
recipe = new Recipe(recipeConfig),
containsFc = recipe.containsFlowControl(),
notUTF8 = options && options.hasOwnProperty("treatAsUtf8") && !options.treatAsUtf8;
let error = false;
let error = false,
progress = 0;
if (containsFc && ENVIRONMENT_IS_WORKER()) self.setOption("attemptHighlight", false);
// Clean up progress
if (progress >= recipeConfig.length) {
progress = 0;
}
if (step) {
// Unset breakpoint on this step
recipe.setBreakpoint(progress, false);
// Set breakpoint on next step
recipe.setBreakpoint(progress + 1, true);
}
// If the previously run operation presented a different value to its
// normal output, we need to recalculate it.
if (recipe.lastOpPresented(progress)) {
progress = 0;
}
// If stepping with flow control, we have to start from the beginning
// but still want to skip all previous breakpoints
if (progress > 0 && containsFc) {
recipe.removeBreaksUpTo(progress);
progress = 0;
}
// If starting from scratch, load data
if (progress === 0) {
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
this.dish.set(input, type);
}
// Load data
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
this.dish.set(input, type);
try {
progress = await recipe.execute(this.dish, progress);
@ -196,6 +168,18 @@ class Chef {
return await newDish.get(type);
}
/**
* Gets the title of a dish and returns it
*
* @param {Dish} dish
* @param {number} [maxLength=100]
* @returns {string}
*/
async getDishTitle(dish, maxLength=100) {
const newDish = new Dish(dish);
return await newDish.getTitle(maxLength);
}
}
export default Chef;

View file

@ -25,6 +25,8 @@ self.chef = new Chef();
self.OpModules = OpModules;
self.OperationConfig = OperationConfig;
self.inputNum = -1;
// Tell the app that the worker has loaded and is ready to operate
self.postMessage({
@ -35,6 +37,9 @@ self.postMessage({
/**
* Respond to message from parent thread.
*
* inputNum is optional and only used for baking multiple inputs.
* Defaults to -1 when one isn't sent with the bake message.
*
* Messages should have the following format:
* {
* action: "bake" | "silentBake",
@ -43,8 +48,9 @@ self.postMessage({
* recipeConfig: {[Object]},
* options: {Object},
* progress: {number},
* step: {boolean}
* } | undefined
* step: {boolean},
* [inputNum=-1]: {number}
* }
* }
*/
self.addEventListener("message", function(e) {
@ -62,6 +68,9 @@ self.addEventListener("message", function(e) {
case "getDishAs":
getDishAs(r.data);
break;
case "getDishTitle":
getDishTitle(r.data);
break;
case "docURL":
// Used to set the URL of the current document so that scripts can be
// imported into an inline worker.
@ -91,30 +100,35 @@ self.addEventListener("message", function(e) {
async function bake(data) {
// Ensure the relevant modules are loaded
self.loadRequiredModules(data.recipeConfig);
try {
self.inputNum = (data.inputNum !== undefined) ? data.inputNum : -1;
const response = await self.chef.bake(
data.input, // The user's input
data.recipeConfig, // The configuration of the recipe
data.options, // Options set by the user
data.progress, // The current position in the recipe
data.step // Whether or not to take one step or execute the whole recipe
data.options // Options set by the user
);
const transferable = (data.input instanceof ArrayBuffer) ? [data.input] : undefined;
self.postMessage({
action: "bakeComplete",
data: Object.assign(response, {
id: data.id
id: data.id,
inputNum: data.inputNum,
bakeId: data.bakeId
})
});
}, transferable);
} catch (err) {
self.postMessage({
action: "bakeError",
data: Object.assign(err, {
id: data.id
})
data: {
error: err.message || err,
id: data.id,
inputNum: data.inputNum
}
});
}
self.inputNum = -1;
}
@ -136,13 +150,33 @@ function silentBake(data) {
*/
async function getDishAs(data) {
const value = await self.chef.getDishAs(data.dish, data.type);
const transferable = (data.type === "ArrayBuffer") ? [value] : undefined;
self.postMessage({
action: "dishReturned",
data: {
value: value,
id: data.id
}
}, transferable);
}
/**
* Gets the dish title
*
* @param {object} data
* @param {Dish} data.dish
* @param {number} data.maxLength
* @param {number} data.id
*/
async function getDishTitle(data) {
const title = await self.chef.getDishTitle(data.dish, data.maxLength);
self.postMessage({
action: "dishReturned",
data: {
value: title,
id: data.id
}
});
}
@ -193,7 +227,28 @@ self.loadRequiredModules = function(recipeConfig) {
self.sendStatusMessage = function(msg) {
self.postMessage({
action: "statusMessage",
data: msg
data: {
message: msg,
inputNum: self.inputNum
}
});
};
/**
* Send progress update to the app.
*
* @param {number} progress
* @param {number} total
*/
self.sendProgressMessage = function(progress, total) {
self.postMessage({
action: "progressMessage",
data: {
progress: progress,
total: total,
inputNum: self.inputNum
}
});
};

View file

@ -8,6 +8,7 @@
import Utils from "./Utils";
import DishError from "./errors/DishError";
import BigNumber from "bignumber.js";
import {detectFileType} from "./lib/FileType";
import log from "loglevel";
/**
@ -141,6 +142,56 @@ class Dish {
}
/**
* Detects the MIME type of the current dish
* @returns {string}
*/
async detectDishType() {
const data = new Uint8Array(this.value.slice(0, 2048)),
types = detectFileType(data);
if (!types.length || !types[0].mime || !types[0].mime === "text/plain") {
return null;
} else {
return types[0].mime;
}
}
/**
* Returns the title of the data up to the specified length
*
* @param {number} maxLength - The maximum title length
* @returns {string}
*/
async getTitle(maxLength) {
let title = "";
const cloned = this.clone();
switch (this.type) {
case Dish.FILE:
title = cloned.value.name;
break;
case Dish.LIST_FILE:
title = `${cloned.value.length} file(s)`;
break;
case Dish.ARRAY_BUFFER:
case Dish.BYTE_ARRAY:
title = await cloned.detectDishType();
if (title === null) {
cloned.value = cloned.value.slice(0, 2048);
title = await cloned.get(Dish.STRING);
}
break;
default:
title = await cloned.get(Dish.STRING);
}
title = title.slice(0, maxLength);
return title;
}
/**
* Translates the data to the given type format.
*

View file

@ -200,7 +200,12 @@ class Recipe {
try {
input = await dish.get(op.inputType);
log.debug("Executing operation");
log.debug(`Executing operation '${op.name}'`);
if (ENVIRONMENT_IS_WORKER()) {
self.sendStatusMessage(`Baking... (${i+1}/${this.opList.length})`);
self.sendProgressMessage(i + 1, this.opList.length);
}
if (op.flowControl) {
// Package up the current state

View file

@ -41,6 +41,7 @@ class App {
this.autoBakePause = false;
this.progress = 0;
this.ingId = 0;
this.timeouts = {};
}
@ -87,7 +88,10 @@ class App {
setTimeout(function() {
document.getElementById("loader-wrapper").remove();
document.body.classList.remove("loaded");
}, 1000);
// Bake initial input
this.manager.input.bakeAll();
}.bind(this), 1000);
// Clear the loading message interval
clearInterval(window.loadingMsgsInt);
@ -96,6 +100,9 @@ class App {
window.removeEventListener("error", window.loadingErrorHandler);
document.dispatchEvent(this.manager.apploaded);
this.manager.input.calcMaxTabs();
this.manager.output.calcMaxTabs();
}
@ -128,7 +135,6 @@ class App {
this.manager.recipe.updateBreakpointIndicator(false);
this.manager.worker.bake(
this.getInput(), // The user's input
this.getRecipeConfig(), // The configuration of the recipe
this.options, // Options set by the user
this.progress, // The current position in the recipe
@ -148,13 +154,46 @@ class App {
if (this.autoBake_ && !this.baking) {
log.debug("Auto-baking");
this.bake();
this.manager.input.inputWorker.postMessage({
action: "autobake",
data: {
activeTab: this.manager.tabs.getActiveInputTab()
}
});
} else {
this.manager.controls.showStaleIndicator();
}
}
/**
* Executes the next step of the recipe.
*/
step() {
if (this.baking) return;
// Reset status using cancelBake
this.manager.worker.cancelBake(true, false);
const activeTab = this.manager.tabs.getActiveInputTab();
if (activeTab === -1) return;
let progress = 0;
if (this.manager.output.outputs[activeTab].progress !== false) {
log.error(this.manager.output.outputs[activeTab]);
progress = this.manager.output.outputs[activeTab].progress;
}
this.manager.input.inputWorker.postMessage({
action: "step",
data: {
activeTab: activeTab,
progress: progress + 1
}
});
}
/**
* Runs a silent bake, forcing the browser to load and cache all the relevant JavaScript code needed
* to do a real bake.
@ -175,24 +214,25 @@ class App {
}
/**
* Gets the user's input data.
*
* @returns {string}
*/
getInput() {
return this.manager.input.get();
}
/**
* Sets the user's input data.
*
* @param {string} input - The string to set the input to
* @param {boolean} [silent=false] - Suppress statechange event
*/
setInput(input, silent=false) {
this.manager.input.set(input, silent);
setInput(input) {
// Get the currently active tab.
// If there isn't one, assume there are no inputs so use inputNum of 1
let inputNum = this.manager.tabs.getActiveInputTab();
if (inputNum === -1) inputNum = 1;
this.manager.input.updateInputValue(inputNum, input);
this.manager.input.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: true
}
});
}
@ -255,9 +295,11 @@ class App {
minSize: minimise ? [0, 0, 0] : [240, 310, 450],
gutterSize: 4,
expandToMin: true,
onDrag: function() {
onDrag: this.debounce(function() {
this.manager.recipe.adjustWidth();
}.bind(this)
this.manager.input.calcMaxTabs();
this.manager.output.calcMaxTabs();
}, 50, "dragSplitter", this, [])
});
this.ioSplitter = Split(["#input", "#output"], {
@ -391,11 +433,12 @@ class App {
this.manager.recipe.initialiseOperationDragNDrop();
}
/**
* Checks for input and recipe in the URI parameters and loads them if present.
* Gets the URI params from the window and parses them to extract the actual values.
*
* @returns {object}
*/
loadURIParams() {
getURIParams() {
// Load query string or hash from URI (depending on which is populated)
// We prefer getting the hash by splitting the href rather than referencing
// location.hash as some browsers (Firefox) automatically URL decode it,
@ -403,8 +446,20 @@ class App {
const params = window.location.search ||
window.location.href.split("#")[1] ||
window.location.hash;
this.uriParams = Utils.parseURIParams(params);
const parsedParams = Utils.parseURIParams(params);
return parsedParams;
}
/**
* Searches the URI parameters for recipe and input parameters.
* If recipe is present, replaces the current recipe with the recipe provided in the URI.
* If input is present, decodes and sets the input to the one provided in the URI.
*
* @fires Manager#statechange
*/
loadURIParams() {
this.autoBakePause = true;
this.uriParams = this.getURIParams();
// Read in recipe from URI params
if (this.uriParams.recipe) {
@ -433,7 +488,7 @@ class App {
if (this.uriParams.input) {
try {
const inputData = fromBase64(this.uriParams.input);
this.setInput(inputData, true);
this.setInput(inputData);
} catch (err) {}
}
@ -522,6 +577,8 @@ class App {
this.columnSplitter.setSizes([20, 30, 50]);
this.ioSplitter.setSizes([50, 50]);
this.manager.recipe.adjustWidth();
this.manager.input.calcMaxTabs();
this.manager.output.calcMaxTabs();
}
@ -656,6 +713,17 @@ class App {
this.progress = 0;
this.autoBake();
this.updateTitle(false, null, true);
}
/**
* Update the page title to contain the new recipe
*
* @param {boolean} includeInput
* @param {string} input
* @param {boolean} [changeUrl=true]
*/
updateTitle(includeInput, input, changeUrl=true) {
// Set title
const recipeConfig = this.getRecipeConfig();
let title = "CyberChef";
@ -674,8 +742,8 @@ class App {
document.title = title;
// Update the current history state (not creating a new one)
if (this.options.updateUrl) {
this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig);
if (this.options.updateUrl && changeUrl) {
this.lastStateUrl = this.manager.controls.generateStateUrl(true, includeInput, input, recipeConfig);
window.history.replaceState({}, title, this.lastStateUrl);
}
}
@ -691,6 +759,29 @@ class App {
this.loadURIParams();
}
/**
* Debouncer to stop functions from being executed multiple times in a
* short space of time
* https://davidwalsh.name/javascript-debounce-function
*
* @param {function} func - The function to be executed after the debounce time
* @param {number} wait - The time (ms) to wait before executing the function
* @param {string} id - Unique ID to reference the timeout for the function
* @param {object} scope - The object to bind to the debounced function
* @param {array} args - Array of arguments to be passed to func
* @returns {function}
*/
debounce(func, wait, id, scope, args) {
return function() {
const later = function() {
func.apply(scope, args);
};
clearTimeout(this.timeouts[id]);
this.timeouts[id] = setTimeout(later, wait);
}.bind(this);
}
}
export default App;

View file

@ -1,354 +0,0 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker";
import Utils from "../core/Utils";
/**
* Waiter to handle events related to the input.
*/
class InputWaiter {
/**
* InputWaiter constructor.
*
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
// Define keys that don't change the input so we don't have to autobake when they are pressed
this.badKeys = [
16, //Shift
17, //Ctrl
18, //Alt
19, //Pause
20, //Caps
27, //Esc
33, 34, 35, 36, //PgUp, PgDn, End, Home
37, 38, 39, 40, //Directional
44, //PrntScrn
91, 92, //Win
93, //Context
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12
144, //Num
145, //Scroll
];
this.loaderWorker = null;
this.fileBuffer = null;
}
/**
* Gets the user's input from the input textarea.
*
* @returns {string}
*/
get() {
return this.fileBuffer || document.getElementById("input-text").value;
}
/**
* Sets the input in the input area.
*
* @param {string|File} input
* @param {boolean} [silent=false] - Suppress statechange event
*
* @fires Manager#statechange
*/
set(input, silent=false) {
const inputText = document.getElementById("input-text");
if (input instanceof File) {
this.setFile(input);
inputText.value = "";
this.setInputInfo(input.size, null);
} else {
inputText.value = input;
this.closeFile();
if (!silent) window.dispatchEvent(this.manager.statechange);
const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
input.count("\n") + 1 : null;
this.setInputInfo(input.length, lines);
}
}
/**
* Shows file details.
*
* @param {File} file
*/
setFile(file) {
// Display file overlay in input area with details
const fileOverlay = document.getElementById("input-file"),
fileName = document.getElementById("input-file-name"),
fileSize = document.getElementById("input-file-size"),
fileType = document.getElementById("input-file-type"),
fileLoaded = document.getElementById("input-file-loaded");
this.fileBuffer = new ArrayBuffer();
fileOverlay.style.display = "block";
fileName.textContent = file.name;
fileSize.textContent = file.size.toLocaleString() + " bytes";
fileType.textContent = file.type || "unknown";
fileLoaded.textContent = "0%";
}
/**
* Displays information about the input.
*
* @param {number} length - The length of the current input string
* @param {number} lines - The number of the lines in the current input string
*/
setInputInfo(length, lines) {
let width = length.toString().length;
width = width < 2 ? 2 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("input-info").innerHTML = msg;
}
/**
* Handler for input change events.
*
* @param {event} e
*
* @fires Manager#statechange
*/
inputChange(e) {
// Ignore this function if the input is a File
if (this.fileBuffer) return;
// Remove highlighting from input and output panes as the offsets might be different now
this.manager.highlighter.removeHighlights();
// Reset recipe progress as any previous processing will be redundant now
this.app.progress = 0;
// Update the input metadata info
const inputText = this.get();
const lines = inputText.length < (this.app.options.ioDisplayThreshold * 1024) ?
inputText.count("\n") + 1 : null;
this.setInputInfo(inputText.length, lines);
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
}
/**
* Handler for input paste events.
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob.
*
* @param {event} e
*/
inputPaste(e) {
const pastedData = e.clipboardData.getData("Text");
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
this.inputChange(e);
} else {
e.preventDefault();
e.stopPropagation();
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
return false;
}
}
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
inputDragover(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
e.target.closest("#input-text,#input-file").classList.add("dropping-file");
}
/**
* Handler for input dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
inputDragleave(e) {
e.stopPropagation();
e.preventDefault();
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
}
/**
* Handler for input drop events.
* Loads the dragged data into the input textarea.
*
* @param {event} e
*/
inputDrop(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
const file = e.dataTransfer.files[0];
const text = e.dataTransfer.getData("Text");
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
if (text) {
this.closeFile();
this.set(text);
return;
}
if (file) {
this.loadFile(file);
}
}
/**
* Handler for open input button events
* Loads the opened data into the input textarea
*
* @param {event} e
*/
inputOpen(e) {
e.preventDefault();
const file = e.srcElement.files[0];
this.loadFile(file);
}
/**
* Handler for messages sent back by the LoaderWorker.
*
* @param {MessageEvent} e
*/
handleLoaderMessage(e) {
const r = e.data;
if (r.hasOwnProperty("progress")) {
const fileLoaded = document.getElementById("input-file-loaded");
fileLoaded.textContent = r.progress + "%";
}
if (r.hasOwnProperty("error")) {
this.app.alert(r.error, 10000);
}
if (r.hasOwnProperty("fileBuffer")) {
log.debug("Input file loaded");
this.fileBuffer = r.fileBuffer;
this.displayFilePreview();
window.dispatchEvent(this.manager.statechange);
}
}
/**
* Shows a chunk of the file in the input behind the file overlay.
*/
displayFilePreview() {
const inputText = document.getElementById("input-text"),
fileSlice = this.fileBuffer.slice(0, 4096);
inputText.style.overflow = "hidden";
inputText.classList.add("blur");
inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
if (this.fileBuffer.byteLength > 4096) {
inputText.value += "[truncated]...";
}
}
/**
* Handler for file close events.
*/
closeFile() {
if (this.loaderWorker) this.loaderWorker.terminate();
this.fileBuffer = null;
document.getElementById("input-file").style.display = "none";
const inputText = document.getElementById("input-text");
inputText.style.overflow = "auto";
inputText.classList.remove("blur");
}
/**
* Loads a file into the input.
*
* @param {File} file
*/
loadFile(file) {
if (file) {
this.closeFile();
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
}
}
/**
* Handler for clear IO events.
* Resets the input, output and info areas.
*
* @fires Manager#statechange
*/
clearIoClick() {
this.closeFile();
this.manager.output.closeFile();
this.manager.highlighter.removeHighlights();
document.getElementById("input-text").value = "";
document.getElementById("output-text").value = "";
document.getElementById("input-info").innerHTML = "";
document.getElementById("output-info").innerHTML = "";
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
window.dispatchEvent(this.manager.statechange);
}
}
export default InputWaiter;

View file

@ -4,18 +4,19 @@
* @license Apache-2.0
*/
import WorkerWaiter from "./WorkerWaiter";
import WindowWaiter from "./WindowWaiter";
import ControlsWaiter from "./ControlsWaiter";
import RecipeWaiter from "./RecipeWaiter";
import OperationsWaiter from "./OperationsWaiter";
import InputWaiter from "./InputWaiter";
import OutputWaiter from "./OutputWaiter";
import OptionsWaiter from "./OptionsWaiter";
import HighlighterWaiter from "./HighlighterWaiter";
import SeasonalWaiter from "./SeasonalWaiter";
import BindingsWaiter from "./BindingsWaiter";
import BackgroundWorkerWaiter from "./BackgroundWorkerWaiter";
import WorkerWaiter from "./waiters/WorkerWaiter";
import WindowWaiter from "./waiters/WindowWaiter";
import ControlsWaiter from "./waiters/ControlsWaiter";
import RecipeWaiter from "./waiters/RecipeWaiter";
import OperationsWaiter from "./waiters/OperationsWaiter";
import InputWaiter from "./waiters/InputWaiter";
import OutputWaiter from "./waiters/OutputWaiter";
import OptionsWaiter from "./waiters/OptionsWaiter";
import HighlighterWaiter from "./waiters/HighlighterWaiter";
import SeasonalWaiter from "./waiters/SeasonalWaiter";
import BindingsWaiter from "./waiters/BindingsWaiter";
import BackgroundWorkerWaiter from "./waiters/BackgroundWorkerWaiter";
import TabWaiter from "./waiters/TabWaiter";
/**
@ -63,6 +64,7 @@ class Manager {
this.controls = new ControlsWaiter(this.app, this);
this.recipe = new RecipeWaiter(this.app, this);
this.ops = new OperationsWaiter(this.app, this);
this.tabs = new TabWaiter(this.app, this);
this.input = new InputWaiter(this.app, this);
this.output = new OutputWaiter(this.app, this);
this.options = new OptionsWaiter(this.app, this);
@ -82,7 +84,9 @@ class Manager {
* Sets up the various components and listeners.
*/
setup() {
this.worker.registerChefWorker();
this.input.setupInputWorker();
this.input.addInput(true);
this.worker.setupChefWorker();
this.recipe.initialiseOperationDragNDrop();
this.controls.initComponents();
this.controls.autoBakeChange();
@ -142,11 +146,11 @@ class Manager {
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
this.addMultiEventListener("#input-text", "keyup", this.input.debounceInputChange, this.input);
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
this.addListeners("#open-file", "change", this.input.inputOpen, this.input);
this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input);
this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input);
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
@ -155,9 +159,31 @@ class Manager {
document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter));
this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input));
document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input));
document.getElementById("btn-next-input-tab").addEventListener("mousedown", this.input.nextTabClick.bind(this.input));
this.addListeners("#btn-next-input-tab,#btn-previous-input-tab", "mouseup", this.input.tabMouseUp, this.input);
this.addListeners("#btn-next-input-tab,#btn-previous-input-tab", "mouseout", this.input.tabMouseUp, this.input);
document.getElementById("btn-go-to-input-tab").addEventListener("click", this.input.goToTab.bind(this.input));
document.getElementById("btn-find-input-tab").addEventListener("click", this.input.findTab.bind(this.input));
this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input);
document.getElementById("input-show-pending").addEventListener("change", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-show-loading").addEventListener("change", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-show-loaded").addEventListener("change", this.input.filterTabSearch.bind(this.input));
this.addListeners("#input-filter-content,#input-filter-filename", "click", this.input.filterOptionClick, this.input);
document.getElementById("input-filter").addEventListener("change", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-filter").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-num-results").addEventListener("change", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-num-results").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
document.getElementById("input-filter-refresh").addEventListener("click", this.input.filterTabSearch.bind(this.input));
this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input);
document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input));
document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input));
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
document.getElementById("save-all-to-file").addEventListener("click", this.output.saveAllClick.bind(this.output));
document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
@ -174,6 +200,25 @@ class Manager {
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
this.addDynamicListener("#output-tabs-wrapper #output-tabs li .output-tab-content", "click", this.output.changeTabClick, this.output);
document.getElementById("btn-previous-output-tab").addEventListener("mousedown", this.output.previousTabClick.bind(this.output));
document.getElementById("btn-next-output-tab").addEventListener("mousedown", this.output.nextTabClick.bind(this.output));
this.addListeners("#btn-next-output-tab,#btn-previous-output-tab", "mouseup", this.output.tabMouseUp, this.output);
this.addListeners("#btn-next-output-tab,#btn-previous-output-tab", "mouseout", this.output.tabMouseUp, this.output);
document.getElementById("btn-go-to-output-tab").addEventListener("click", this.output.goToTab.bind(this.output));
document.getElementById("btn-find-output-tab").addEventListener("click", this.output.findTab.bind(this.output));
document.getElementById("output-show-pending").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-show-baking").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-show-baked").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-show-stale").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-show-errored").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-content-filter").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-content-filter").addEventListener("keyup", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-num-results").addEventListener("change", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-num-results").addEventListener("keyup", this.output.filterTabSearch.bind(this.output));
document.getElementById("output-filter-refresh").addEventListener("click", this.output.filterTabSearch.bind(this.output));
this.addDynamicListener(".output-filter-result", "click", this.output.filterItemClick, this.output);
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
@ -186,6 +231,7 @@ class Manager {
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);
document.getElementById("theme").addEventListener("change", this.options.themeChange.bind(this.options));
document.getElementById("logLevel").addEventListener("change", this.options.logLevelChange.bind(this.options));
document.getElementById("imagePreview").addEventListener("change", this.input.renderFileThumb.bind(this.input));
// Misc
window.addEventListener("keydown", this.bindings.parseInput.bind(this.bindings));
@ -307,7 +353,6 @@ class Manager {
}
}
}
}
export default Manager;

View file

@ -1,547 +0,0 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../core/Utils";
import FileSaver from "file-saver";
/**
* Waiter to handle events related to the output.
*/
class OutputWaiter {
/**
* OutputWaiter constructor.
*
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
this.dishBuffer = null;
this.dishStr = null;
}
/**
* Gets the output string from the output textarea.
*
* @returns {string}
*/
get() {
return document.getElementById("output-text").value;
}
/**
* Sets the output in the output textarea.
*
* @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer
* @param {string} type - The data type of the output
* @param {number} duration - The length of time (ms) it took to generate the output
* @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer
*/
async set(data, type, duration, preserveBuffer) {
log.debug("Output type: " + type);
const outputText = document.getElementById("output-text");
const outputHtml = document.getElementById("output-html");
const outputFile = document.getElementById("output-file");
const outputHighlighter = document.getElementById("output-highlighter");
const inputHighlighter = document.getElementById("input-highlighter");
let scriptElements, lines, length;
if (!preserveBuffer) {
this.closeFile();
this.dishStr = null;
document.getElementById("show-file-overlay").style.display = "none";
}
switch (type) {
case "html":
outputText.style.display = "none";
outputHtml.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = data;
// Execute script sections
scriptElements = outputHtml.querySelectorAll("script");
for (let i = 0; i < scriptElements.length; i++) {
try {
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
} catch (err) {
log.error(err);
}
}
await this.getDishStr();
length = this.dishStr.length;
lines = this.dishStr.count("\n") + 1;
break;
case "ArrayBuffer":
outputText.style.display = "block";
outputHtml.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = "";
length = data.byteLength;
this.setFile(data);
break;
case "string":
default:
outputText.style.display = "block";
outputHtml.style.display = "none";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
outputText.value = Utils.printable(data, true);
outputHtml.innerHTML = "";
lines = data.count("\n") + 1;
length = data.length;
this.dishStr = data;
break;
}
this.manager.highlighter.removeHighlights();
this.setOutputInfo(length, lines, duration);
this.backgroundMagic();
}
/**
* Shows file details.
*
* @param {ArrayBuffer} buf
*/
setFile(buf) {
this.dishBuffer = buf;
const file = new File([buf], "output.dat");
// Display file overlay in output area with details
const fileOverlay = document.getElementById("output-file"),
fileSize = document.getElementById("output-file-size");
fileOverlay.style.display = "block";
fileSize.textContent = file.size.toLocaleString() + " bytes";
// Display preview slice in the background
const outputText = document.getElementById("output-text"),
fileSlice = this.dishBuffer.slice(0, 4096);
outputText.classList.add("blur");
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
}
/**
* Removes the output file and nulls its memory.
*/
closeFile() {
this.dishBuffer = null;
document.getElementById("output-file").style.display = "none";
document.getElementById("output-text").classList.remove("blur");
}
/**
* Handler for file download events.
*/
async downloadFile() {
this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
await this.getDishBuffer();
const file = new File([this.dishBuffer], this.filename);
if (this.filename) FileSaver.saveAs(file, this.filename, false);
}
/**
* Handler for file slice display events.
*/
displayFileSlice() {
const startTime = new Date().getTime(),
showFileOverlay = document.getElementById("show-file-overlay"),
sliceFromEl = document.getElementById("output-file-slice-from"),
sliceToEl = document.getElementById("output-file-slice-to"),
sliceFrom = parseInt(sliceFromEl.value, 10),
sliceTo = parseInt(sliceToEl.value, 10),
str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo));
document.getElementById("output-text").classList.remove("blur");
showFileOverlay.style.display = "block";
this.set(str, "string", new Date().getTime() - startTime, true);
}
/**
* Handler for show file overlay events.
*
* @param {Event} e
*/
showFileOverlayClick(e) {
const outputFile = document.getElementById("output-file"),
showFileOverlay = e.target;
document.getElementById("output-text").classList.add("blur");
outputFile.style.display = "block";
showFileOverlay.style.display = "none";
this.setOutputInfo(this.dishBuffer.byteLength, null, 0);
}
/**
* Displays information about the output.
*
* @param {number} length - The length of the current output string
* @param {number} lines - The number of the lines in the current output string
* @param {number} duration - The length of time (ms) it took to generate the output
*/
setOutputInfo(length, lines, duration) {
let width = length.toString().length;
width = width < 4 ? 4 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("output-info").innerHTML = msg;
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
}
/**
* Handler for save click events.
* Saves the current output to a file.
*/
saveClick() {
this.downloadFile();
}
/**
* Handler for copy click events.
* Copies the output to the clipboard.
*/
async copyClick() {
await this.getDishStr();
// Create invisible textarea to populate with the raw dish string (not the printable version that
// contains dots instead of the actual bytes)
const textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.top = 0;
textarea.style.left = 0;
textarea.style.width = 0;
textarea.style.height = 0;
textarea.style.border = "none";
textarea.value = this.dishStr;
document.body.appendChild(textarea);
// Select and copy the contents of this textarea
let success = false;
try {
textarea.select();
success = this.dishStr && document.execCommand("copy");
} catch (err) {
success = false;
}
if (success) {
this.app.alert("Copied raw output successfully.", 2000);
} else {
this.app.alert("Sorry, the output could not be copied.", 3000);
}
// Clean up
document.body.removeChild(textarea);
}
/**
* Handler for switch click events.
* Moves the current output into the input textarea.
*/
async switchClick() {
this.switchOrigData = this.manager.input.get();
document.getElementById("undo-switch").disabled = false;
if (this.dishBuffer) {
this.manager.input.setFile(new File([this.dishBuffer], "output.dat"));
this.manager.input.handleLoaderMessage({
data: {
progress: 100,
fileBuffer: this.dishBuffer
}
});
} else {
await this.getDishStr();
this.app.setInput(this.dishStr);
}
}
/**
* Handler for undo switch click events.
* Removes the output from the input and replaces the input that was removed.
*/
undoSwitchClick() {
this.app.setInput(this.switchOrigData);
const undoSwitch = document.getElementById("undo-switch");
undoSwitch.disabled = true;
$(undoSwitch).tooltip("hide");
}
/**
* Handler for maximise output click events.
* Resizes the output frame to be as large as possible, or restores it to its original size.
*/
maximiseOutputClick(e) {
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
this.app.initialiseSplitter(true);
this.app.columnSplitter.collapse(0);
this.app.columnSplitter.collapse(1);
this.app.ioSplitter.collapse(0);
$(el).attr("data-original-title", "Restore output pane");
el.querySelector("i").innerHTML = "fullscreen_exit";
} else {
$(el).attr("data-original-title", "Maximise output pane");
el.querySelector("i").innerHTML = "fullscreen";
this.app.initialiseSplitter(false);
this.app.resetLayout();
}
}
/**
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
*/
saveBombe() {
this.bombeEl = document.getElementById("bombe");
this.bombeEl.parentNode.removeChild(this.bombeEl);
}
/**
* Shows or hides the output loading screen.
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
* intensive, so we remove it from the DOM when not in use. We only show it if the
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
* it is ready to fade in without stuttering.
*
* @param {boolean} value - true == show loader
*/
toggleLoader(value) {
clearTimeout(this.appendBombeTimeout);
clearTimeout(this.outputLoaderTimeout);
const outputLoader = document.getElementById("output-loader"),
outputElement = document.getElementById("output-text"),
animation = document.getElementById("output-loader-animation");
if (value) {
this.manager.controls.hideStaleIndicator();
// Start a timer to add the Bombe to the DOM just before we make it
// visible so that there is no stuttering
this.appendBombeTimeout = setTimeout(function() {
animation.appendChild(this.bombeEl);
}.bind(this), 150);
// Show the loading screen
this.outputLoaderTimeout = setTimeout(function() {
outputElement.disabled = true;
outputLoader.style.visibility = "visible";
outputLoader.style.opacity = 1;
this.manager.controls.toggleBakeButtonFunction(true);
}.bind(this), 200);
} else {
// Remove the Bombe from the DOM to save resources
this.outputLoaderTimeout = setTimeout(function () {
try {
animation.removeChild(this.bombeEl);
} catch (err) {}
}.bind(this), 500);
outputElement.disabled = false;
outputLoader.style.opacity = 0;
outputLoader.style.visibility = "hidden";
this.manager.controls.toggleBakeButtonFunction(false);
this.setStatusMsg("");
}
}
/**
* Sets the baking status message value.
*
* @param {string} msg
*/
setStatusMsg(msg) {
const el = document.querySelector("#output-loader .loading-msg");
el.textContent = msg;
}
/**
* Returns true if the output contains carriage returns
*
* @returns {boolean}
*/
async containsCR() {
await this.getDishStr();
return this.dishStr.indexOf("\r") >= 0;
}
/**
* Retrieves the current dish as a string, returning the cached version if possible.
*
* @returns {string}
*/
async getDishStr() {
if (this.dishStr) return this.dishStr;
this.dishStr = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "string", r => {
resolve(r.value);
});
});
return this.dishStr;
}
/**
* Retrieves the current dish as an ArrayBuffer, returning the cached version if possible.
*
* @returns {ArrayBuffer}
*/
async getDishBuffer() {
if (this.dishBuffer) return this.dishBuffer;
this.dishBuffer = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => {
resolve(r.value);
});
});
return this.dishBuffer;
}
/**
* Triggers the BackgroundWorker to attempt Magic on the current output.
*/
backgroundMagic() {
this.hideMagicButton();
if (!this.app.options.autoMagic) return;
const sample = this.dishStr ? this.dishStr.slice(0, 1000) :
this.dishBuffer ? this.dishBuffer.slice(0, 1000) : "";
if (sample.length) {
this.manager.background.magic(sample);
}
}
/**
* Handles the results of a background Magic call.
*
* @param {Object[]} options
*/
backgroundMagicResult(options) {
if (!options.length ||
!options[0].recipe.length)
return;
const currentRecipeConfig = this.app.getRecipeConfig();
const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
const opSequence = options[0].recipe.map(o => o.op).join(", ");
this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
}
/**
* Handler for Magic click events.
*
* Loads the Magic recipe.
*
* @fires Manager#statechange
*/
magicClick() {
const magicButton = document.getElementById("magic");
this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
window.dispatchEvent(this.manager.statechange);
this.hideMagicButton();
}
/**
* Displays the Magic button with a title and adds a link to a complete recipe.
*
* @param {string} opSequence
* @param {string} result
* @param {Object[]} recipeConfig
*/
showMagicButton(opSequence, result, recipeConfig) {
const magicButton = document.getElementById("magic");
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
magicButton.classList.remove("hidden");
}
/**
* Hides the Magic button and resets its values.
*/
hideMagicButton() {
const magicButton = document.getElementById("magic");
magicButton.classList.add("hidden");
magicButton.setAttribute("data-recipe", "");
magicButton.setAttribute("data-original-title", "Magic!");
}
/**
* Handler for extract file events.
*
* @param {Event} e
*/
async extractFileClick(e) {
e.preventDefault();
e.stopPropagation();
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
const blobURL = el.getAttribute("blob-url");
const fileName = el.getAttribute("file-name");
const blob = await fetch(blobURL).then(r => r.blob());
this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
}
}
export default OutputWaiter;

View file

@ -1,239 +0,0 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker";
/**
* Waiter to handle conversations with the ChefWorker.
*/
class WorkerWaiter {
/**
* WorkerWaiter constructor.
*
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
this.callbacks = {};
this.callbackID = 0;
}
/**
* Sets up the ChefWorker and associated listeners.
*/
registerChefWorker() {
log.debug("Registering new ChefWorker");
this.chefWorker = new ChefWorker();
this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this));
this.setLogLevel();
let docURL = document.location.href.split(/[#?]/)[0];
const index = docURL.lastIndexOf("/");
if (index > 0) {
docURL = docURL.substring(0, index);
}
this.chefWorker.postMessage({"action": "docURL", "data": docURL});
}
/**
* Handler for messages sent back by the ChefWorker.
*
* @param {MessageEvent} e
*/
handleChefMessage(e) {
const r = e.data;
log.debug("Receiving '" + r.action + "' from ChefWorker");
switch (r.action) {
case "bakeComplete":
this.bakingComplete(r.data);
break;
case "bakeError":
this.app.handleError(r.data);
this.setBakingStatus(false);
break;
case "dishReturned":
this.callbacks[r.data.id](r.data);
break;
case "silentBakeComplete":
break;
case "workerLoaded":
this.app.workerLoaded = true;
log.debug("ChefWorker loaded");
this.app.loaded();
break;
case "statusMessage":
this.manager.output.setStatusMsg(r.data);
break;
case "optionUpdate":
log.debug(`Setting ${r.data.option} to ${r.data.value}`);
this.app.options[r.data.option] = r.data.value;
break;
case "setRegisters":
this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers);
break;
case "highlightsCalculated":
this.manager.highlighter.displayHighlights(r.data.pos, r.data.direction);
break;
default:
log.error("Unrecognised message from ChefWorker", e);
break;
}
}
/**
* Updates the UI to show if baking is in process or not.
*
* @param {bakingStatus}
*/
setBakingStatus(bakingStatus) {
this.app.baking = bakingStatus;
this.manager.output.toggleLoader(bakingStatus);
}
/**
* Cancels the current bake by terminating the ChefWorker and creating a new one.
*/
cancelBake() {
this.chefWorker.terminate();
this.registerChefWorker();
this.setBakingStatus(false);
this.manager.controls.showStaleIndicator();
}
/**
* Handler for completed bakes.
*
* @param {Object} response
*/
bakingComplete(response) {
this.setBakingStatus(false);
if (!response) return;
if (response.error) {
this.app.handleError(response.error);
}
this.app.progress = response.progress;
this.app.dish = response.dish;
this.manager.recipe.updateBreakpointIndicator(response.progress);
this.manager.output.set(response.result, response.type, response.duration);
log.debug("--- Bake complete ---");
}
/**
* Asks the ChefWorker to bake the current input using the current recipe.
*
* @param {string} input
* @param {Object[]} recipeConfig
* @param {Object} options
* @param {number} progress
* @param {boolean} step
*/
bake(input, recipeConfig, options, progress, step) {
this.setBakingStatus(true);
this.chefWorker.postMessage({
action: "bake",
data: {
input: input,
recipeConfig: recipeConfig,
options: options,
progress: progress,
step: step
}
});
}
/**
* Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant
* JavaScript code needed to do a real bake.
*
* @param {Object[]} [recipeConfig]
*/
silentBake(recipeConfig) {
this.chefWorker.postMessage({
action: "silentBake",
data: {
recipeConfig: recipeConfig
}
});
}
/**
* Asks the ChefWorker to calculate highlight offsets if possible.
*
* @param {Object[]} recipeConfig
* @param {string} direction
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
highlight(recipeConfig, direction, pos) {
this.chefWorker.postMessage({
action: "highlight",
data: {
recipeConfig: recipeConfig,
direction: direction,
pos: pos
}
});
}
/**
* Asks the ChefWorker to return the dish as the specified type
*
* @param {Dish} dish
* @param {string} type
* @param {Function} callback
*/
getDishAs(dish, type, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
this.chefWorker.postMessage({
action: "getDishAs",
data: {
dish: dish,
type: type,
id: id
}
});
}
/**
* Sets the console log level in the worker.
*
* @param {string} level
*/
setLogLevel(level) {
if (!this.chefWorker) return;
this.chefWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
});
}
}
export default WorkerWaiter;

View file

@ -218,9 +218,16 @@
<div class="title no-select">
<label for="input-text">Input</label>
<span class="float-right">
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" onclick="document.getElementById('open-file').click();">
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab">
<i class="material-icons">add</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-toggle="tooltip" title="Open folder as input">
<i class="material-icons">folder_open</i>
<input type="file" id="open-folder" style="display: none" multiple directory webkitdirectory>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input">
<i class="material-icons">input</i>
<input type="file" id="open-file" style="display: none">
<input type="file" id="open-file" style="display: none" multiple>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output">
<i class="material-icons">delete</i>
@ -229,17 +236,42 @@
<i class="material-icons">view_compact</i>
</button>
</span>
<div class="io-info" id="input-files-info"></div>
<div class="io-info" id="input-info"></div>
<div class="io-info" id="input-selection-info"></div>
</div>
<div class="textarea-wrapper no-select">
<div id="input-tabs-wrapper" style="display: none;" class="no-select">
<span id="btn-previous-input-tab" class="input-tab-buttons">
&lt;
</span>
<span id="btn-input-tab-dropdown" class="input-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
···
</span>
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
<a id="btn-go-to-input-tab" class="dropdown-item">
Go to tab
</a>
<a id="btn-find-input-tab" class="dropdown-item">
Find tab
</a>
<a id="btn-close-all-tabs" class="dropdown-item">
Close all tabs
</a>
</div>
<span id="btn-next-input-tab" class="input-tab-buttons">
&gt;
</span>
<ul id="input-tabs">
</ul>
</div>
<div class="textarea-wrapper no-select input-wrapper" id="input-wrapper">
<div id="input-highlighter" class="no-select"></div>
<textarea id="input-text" spellcheck="false"></textarea>
<div id="input-file">
<div class="file-overlay"></div>
<textarea id="input-text" class="input-text" spellcheck="false"></textarea>
<div class="input-file" id="input-file">
<div class="file-overlay" id="file-overlay"></div>
<div style="position: relative; height: 100%;">
<div class="io-card card">
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon" id="input-file-thumbnail"/>
<div class="card-body">
<button type="button" class="close" id="input-file-close">&times;</button>
Name: <span id="input-file-name"></span><br>
@ -257,13 +289,16 @@
<div class="title no-select">
<label for="output-text">Output</label>
<span class="float-right">
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-toggle="tooltip" title="Save all outputs to a zip file" style="display: none">
<i class="material-icons">archive</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-to-file" data-toggle="tooltip" title="Save output to file">
<i class="material-icons">save</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="copy-output" data-toggle="tooltip" title="Copy raw output to the clipboard">
<i class="material-icons">content_copy</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Move output to input">
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Replace input with output">
<i class="material-icons">open_in_browser</i>
</button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="undo-switch" data-toggle="tooltip" title="Undo" disabled="disabled">
@ -273,6 +308,7 @@
<i class="material-icons">fullscreen</i>
</button>
</span>
<div class="io-info" id="bake-info"></div>
<div class="io-info" id="output-info"></div>
<div class="io-info" id="output-selection-info"></div>
<button type="button" class="btn btn-primary bmd-btn-icon hidden" id="magic" data-toggle="tooltip" title="Magic!" data-html="true">
@ -284,38 +320,61 @@
<i class="material-icons">access_time</i>
</span>
</div>
<div class="textarea-wrapper">
<div id="output-highlighter" class="no-select"></div>
<div id="output-html"></div>
<textarea id="output-text" readonly="readonly" spellcheck="false"></textarea>
<img id="show-file-overlay" aria-hidden="true" src="<%- require('../static/images/file-32x32.png') %>" alt="Show file overlay" title="Show file overlay"/>
<div id="output-file">
<div class="file-overlay"></div>
<div style="position: relative; height: 100%;">
<div class="io-card card">
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
<div class="card-body">
Size: <span id="output-file-size"></span><br>
<button id="output-file-download" type="button" class="btn btn-primary btn-outline">Download</button>
<div class="input-group">
<span class="input-group-btn">
<button id="output-file-slice" type="button" class="btn btn-secondary bmd-btn-icon" title="View slice">
<i class="material-icons">search</i>
</button>
</span>
<input type="number" class="form-control" id="output-file-slice-from" placeholder="From" value="0" step="1024" min="0">
<div class="input-group-addon">to</div>
<input type="number" class="form-control" id="output-file-slice-to" placeholder="To" value="2048" step="1024" min="0">
<div id="output-wrapper">
<div id="output-tabs-wrapper" style="display: none" class="no-select">
<span id="btn-previous-output-tab" class="output-tab-buttons">
&lt;
</span>
<span id="btn-output-tab-dropdown" class="output-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
···
</span>
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
<a id="btn-go-to-output-tab" class="dropdown-item">
Go to tab
</a>
<a id="btn-find-output-tab" class="dropdown-item">
Find tab
</a>
</div>
<span id="btn-next-output-tab" class="output-tab-buttons">
&gt;
</span>
<ul id="output-tabs">
</ul>
</div>
<div class="textarea-wrapper">
<div id="output-highlighter" class="no-select"></div>
<div id="output-html"></div>
<textarea id="output-text" readonly="readonly" spellcheck="false"></textarea>
<img id="show-file-overlay" aria-hidden="true" src="<%- require('../static/images/file-32x32.png') %>" alt="Show file overlay" title="Show file overlay"/>
<div id="output-file">
<div class="file-overlay"></div>
<div style="position: relative; height: 100%;">
<div class="io-card card">
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
<div class="card-body">
Size: <span id="output-file-size"></span><br>
<button id="output-file-download" type="button" class="btn btn-primary btn-outline">Download</button>
<div class="input-group">
<span class="input-group-btn">
<button id="output-file-slice" type="button" class="btn btn-secondary bmd-btn-icon" title="View slice">
<i class="material-icons">search</i>
</button>
</span>
<input type="number" class="form-control" id="output-file-slice-from" placeholder="From" value="0" step="1024" min="0">
<div class="input-group-addon">to</div>
<input type="number" class="form-control" id="output-file-slice-to" placeholder="To" value="2048" step="1024" min="0">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="output-loader">
<div id="output-loader-animation">
<object id="bombe" data="<%- require('../static/images/bombe.svg') %>" width="100%" height="100%"></object>
<div id="output-loader">
<div id="output-loader-animation">
<object id="bombe" data="<%- require('../static/images/bombe.svg') %>" width="100%" height="100%"></object>
</div>
<div class="loading-msg"></div>
</div>
<div class="loading-msg"></div>
</div>
</div>
</div>
@ -425,6 +484,8 @@
<option value="classic">Classic</option>
<option value="dark">Dark</option>
<option value="geocities">GeoCities</option>
<option value="solarizedDark">Solarized Dark</option>
<option value="solarizedLight">Solarized Light</option>
</select>
</div>
@ -498,6 +559,20 @@
Attempt to detect encoded data automagically
</label>
</div>
<div class="checkbox option-item">
<label for="imagePreview">
<input type="checkbox" option="imagePreview" id="imagePreview">
Render a preview of the input if it's detected to be an image.
</label>
</div>
<div class="checkbox option-item">
<label for="syncTabs">
<input type="checkbox" option="syncTabs" id="syncTabs">
Keep the current tab in sync between the input and output.
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
@ -599,7 +674,7 @@
</a>
<div class="collapse" id="faq-load-files">
<p>Yes! Just drag your file over the input box and drop it.</p>
<p>CyberChef can handle files up to around 500MB (depending on your browser), however some of the operations may take a very long time to run over this much data.</p>
<p>CyberChef can handle files up to around 2GB (depending on your browser), however some of the operations may take a very long time to run over this much data.</p>
<p>If the output is larger than a certain threshold (default 1MiB), it will be presented to you as a file available for download. Slices of the file can be viewed in the output if you need to inspect them.</p>
</div>
<br>
@ -688,5 +763,125 @@
</div>
</div>
<div class="modal" id="input-tab-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Find Input Tab</h5>
</div>
<div class="modal-body" id="input-tab-body">
<h6>Load Status</h6>
<div id="input-find-options">
<ul id="input-find-options-checkboxes">
<li class="checkbox input-find-option">
<label for="input-show-pending">
<input type="checkbox" id="input-show-pending" checked="">
Pending
</label>
</li>
<li class="checkbox input-find-option">
<label for="input-show-loading">
<input type="checkbox" id="input-show-loading" checked="">
Loading
</label>
</li>
<li class="checkbox input-find-option">
<label for="input-show-loaded">
<input type="checkbox" id="input-show-loaded" checked="">
Loaded
</label>
</li>
</ul>
</div>
<div class="form-group input-group">
<div class="toggle-string">
<label for="input-filter" class="bmd-label-floating toggle-string">Filter (regex)</label>
<input type="text" class="form-control toggle-string" id="input-filter">
</div>
<div class="input-group-append">
<button class="btn btn-secondary dropdown-toggle" id="input-filter-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">CONTENT</button>
<div class="dropdown-menu toggle-dropdown">
<a class="dropdown-item" id="input-filter-content">Content</a>
<a class="dropdown-item" id="input-filter-filename">Filename</a>
</div>
</div>
</div>
<div class="form-group input-find-option" id="input-num-results-container">
<label for="input-num-results" class="bmd-label-floating">Number of results</label>
<input type="number" class="form-control" id="input-num-results" value="20" min="1">
</div>
<div style="clear:both"></div>
<h6>Results</h6>
<ul id="input-search-results"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="input-filter-refresh">Refresh</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" id="output-tab-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Find Output Tab</h5>
</div>
<div class="modal-body" id="output-tab-body">
<h6>Bake Status</h6>
<div id="output-find-options">
<ul id="output-find-options-checkboxes">
<li class="checkbox output-find-option">
<label for="output-show-pending">
<input type="checkbox" id="output-show-pending" checked="">
Pending
</label>
</li>
<li class="checkbox output-find-option">
<label for="output-show-baking">
<input type="checkbox" id="output-show-baking" checked="">
Baking
</label>
</li>
<li class="checkbox output-find-option">
<label for="output-show-baked">
<input type="checkbox" id="output-show-baked" checked="">
Baked
</label>
</li>
<li class="checkbox output-find-option">
<label for="output-show-stale">
<input type="checkbox" id="output-show-stale" checked="">
Stale
</label>
</li>
<li class="checkbox output-find-option">
<label for="output-show-errored">
<input type="checkbox" id="output-show-errored" checked="">
Errored
</label>
</li>
</ul>
<div class="form-group output-find-option">
<label for="output-content-filter" class="bmd-label-floating">Content filter (regex)</label>
<input type="text" class="form-control" id="output-content-filter">
</div>
<div class="form-group output-find-option" id="output-num-results-container">
<label for="output-num-results" class="bmd-label-floating">Number of results</label>
<input type="number" class="form-control" id="output-num-results" value="20">
</div>
</div>
<br>
<h6>Results</h6>
<ul id="output-search-results"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="output-filter-refresh">Refresh</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -52,6 +52,8 @@ function main() {
ioDisplayThreshold: 512,
logLevel: "info",
autoMagic: true,
imagePreview: true,
syncTabs: true
};
document.removeEventListener("DOMContentLoaded", main, false);

View file

@ -82,7 +82,43 @@ div.toggle-string {
.operation .is-focused [class*=' bmd-label'],
.operation .is-focused label,
.operation .checkbox label:hover {
color: #1976d2;
color: var(--input-highlight-colour);
}
.ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
.ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
border-color: var(--input-border-colour);
color: var(--input-highlight-colour);
}
.ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
.ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
border-color: var(--input-highlight-colour);
color: var(--input-highlight-colour);
}
.disabled .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
.disabled .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
.disabled .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
.disabled .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
border-color: var(--disabled-font-colour);
color: var(--disabled-font-colour);
}
.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
border-color: var(--breakpoint-font-colour);
color: var(--breakpoint-font-colour);
}
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
border-color: var(--fc-breakpoint-operation-font-colour);
color: var(--fc-breakpoint-operation-font-colour);
}
.operation .form-control {
@ -97,7 +133,7 @@ div.toggle-string {
.operation .form-control:hover {
background-image:
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(25, 118, 210, 0) 2px),
linear-gradient(to top, rgba(0, 0, 0, 0.26) 1px, rgba(0, 0, 0, 0) 1px);
filter: brightness(97%);
}
@ -105,7 +141,7 @@ div.toggle-string {
.operation .form-control:focus {
background-color: var(--arg-background);
background-image:
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(25, 118, 210, 0) 2px),
linear-gradient(to top, rgba(0, 0, 0, 0.26) 1px, rgba(0, 0, 0, 0) 1px);
filter: brightness(100%);
}
@ -205,19 +241,19 @@ div.toggle-string {
}
.disable-icon {
color: #9e9e9e;
color: var(--disable-icon-colour);
}
.disable-icon-selected {
color: #f44336;
color: var(--disable-icon-selected-colour);
}
.breakpoint {
color: #9e9e9e;
color: var(--breakpoint-icon-colour);
}
.breakpoint-selected {
color: #f44336;
color: var(--breakpoint-icon-selected-colour);
}
.break {

View file

@ -8,6 +8,7 @@
:root {
--title-height: 48px;
--tab-height: 40px;
}
.title {
@ -52,6 +53,7 @@
line-height: 30px;
background-color: var(--primary-background-colour);
flex-direction: row;
padding-left: 10px;
}
.io-card.card:hover {
@ -60,10 +62,16 @@
.io-card.card>img {
float: left;
width: 128px;
height: 128px;
margin-left: 10px;
margin-top: 11px;
width: auto;
height: auto;
max-width: 128px;
max-height: 128px;
margin-left: auto;
margin-top: auto;
margin-right: auto;
margin-bottom: auto;
padding: 0px;
}
.io-card.card .card-body .close {

View file

@ -10,6 +10,8 @@
@import "./themes/_classic.css";
@import "./themes/_dark.css";
@import "./themes/_geocities.css";
@import "./themes/_solarizedDark.css";
@import "./themes/_solarizedLight.css";
/* Utilities */
@import "./utils/_overrides.css";

View file

@ -22,6 +22,10 @@
padding-right: 10px;
}
#banner a {
color: var(--banner-url-colour);
}
#notice-wrapper {
text-align: center;
overflow: hidden;

View file

@ -40,6 +40,12 @@
cursor: pointer;
}
#auto-bake-label .check,
#auto-bake-label .check::before {
border-color: var(--input-highlight-colour);
color: var(--input-highlight-colour);
}
#auto-bake-label .checkbox-decorator {
position: relative;
}
@ -51,3 +57,15 @@
#controls .btn {
border-radius: 30px;
}
.spin {
animation-name: spin;
animation-duration: 3s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes spin {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}

View file

@ -24,18 +24,172 @@
word-wrap: break-word;
}
#output-wrapper{
margin: 0;
padding: 0;
}
#output-wrapper .textarea-wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow: hidden;
pointer-events: auto;
}
#output-html {
display: none;
overflow-y: auto;
-moz-padding-start: 1px; /* Fixes bug in Firefox */
}
.textarea-wrapper {
position: absolute;
top: var(--title-height);
bottom: 0;
#input-tabs-wrapper #input-tabs,
#output-tabs-wrapper #output-tabs {
list-style: none;
background-color: var(--title-background-colour);
padding: 0;
margin: 0;
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--primary-border-colour);
border-left: 1px solid var(--primary-border-colour);
height: var(--tab-height);
clear: none;
}
#input-tabs li,
#output-tabs li {
display: flex;
flex-direction: row;
width: 100%;
min-width: 120px;
float: left;
padding: 0px;
text-align: center;
border-right: 1px solid var(--primary-border-colour);
height: var(--tab-height);
vertical-align: middle;
}
#input-tabs li:hover,
#output-tabs li:hover {
cursor: pointer;
background-color: var(--primary-background-colour);
}
.active-input-tab,
.active-output-tab {
font-weight: bold;
background-color: var(--primary-background-colour);
}
.input-tab-content+.btn-close-tab {
display: block;
margin-top: auto;
margin-bottom: auto;
margin-right: 2px;
}
.input-tab-content+.btn-close-tab i {
font-size: 0.8em;
}
.input-tab-buttons,
.output-tab-buttons {
width: 25px;
text-align: center;
margin: 0;
height: var(--tab-height);
line-height: var(--tab-height);
font-weight: bold;
background-color: var(--title-background-colour);
border-bottom: 1px solid var(--primary-border-colour);
}
.input-tab-buttons:hover,
.output-tab-buttons:hover {
cursor: pointer;
background-color: var(--primary-background-colour);
}
#btn-next-input-tab,
#btn-input-tab-dropdown,
#btn-next-output-tab,
#btn-output-tab-dropdown {
float: right;
}
#btn-previous-input-tab,
#btn-previous-output-tab {
float: left;
}
#btn-close-all-tabs {
color: var(--breakpoint-font-colour) !important;
}
.input-tab-content,
.output-tab-content {
width: 100%;
max-width: 100%;
padding-left: 5px;
padding-right: 5px;
padding-top: 10px;
padding-bottom: 10px;
height: var(--tab-height);
vertical-align: middle;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.btn-close-tab {
height: var(--tab-height);
vertical-align: middle;
width: fit-content;
}
.tabs-left > li:first-child {
box-shadow: 15px 0px 15px -15px var(--primary-border-colour) inset;
}
.tabs-right > li:last-child {
box-shadow: -15px 0px 15px -15px var(--primary-border-colour) inset;
}
#input-wrapper,
#output-wrapper,
#input-wrapper > * ,
#output-wrapper > .textarea-wrapper > div,
#output-wrapper > .textarea-wrapper > textarea {
height: calc(100% - var(--title-height));
}
#input-wrapper.show-tabs,
#input-wrapper.show-tabs > *,
#output-wrapper.show-tabs,
#output-wrapper.show-tabs > .textarea-wrapper > div,
#output-wrapper.show-tabs > .textarea-wrapper > textarea {
height: calc(100% - var(--tab-height) - var(--title-height));
}
#output-wrapper > .textarea-wrapper > #output-html {
height: 100%;
}
#show-file-overlay {
height: 32px;
}
.input-wrapper.textarea-wrapper {
width: 100%;
box-sizing: border-box;
overflow: hidden;
pointer-events: auto;
}
.textarea-wrapper textarea,
@ -49,9 +203,8 @@
#output-highlighter {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
padding: 3px;
margin: 0;
overflow: hidden;
@ -61,14 +214,14 @@
color: #fff;
background-color: transparent;
border: none;
pointer-events: none;
}
#output-loader {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
background-color: var(--primary-background-colour);
visibility: hidden;
@ -105,9 +258,8 @@
#output-file {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: none;
}
@ -122,7 +274,7 @@
#show-file-overlay {
position: absolute;
right: 15px;
top: 15px;
top: calc(var(--title-height) + 10px);
cursor: pointer;
display: none;
}
@ -147,7 +299,6 @@
.dropping-file {
border: 5px dashed var(--drop-file-border-colour) !important;
margin: -5px;
}
#stale-indicator {
@ -185,3 +336,103 @@
#magic svg path {
fill: var(--primary-font-colour);
}
#input-find-options,
#output-find-options {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
}
#input-tab-body .form-group.input-group,
#output-tab-body .form-group.input-group {
width: 70%;
float: left;
margin-bottom: 2rem;
}
.input-find-option .toggle-string {
width: 70%;
display: inline-block;
}
.input-find-option-append button {
border-top-right-radius: 4px;
background-color: var(--arg-background) !important;
margin: unset;
}
.input-find-option-append button:hover {
filter: brightness(97%);
}
.form-group.output-find-option {
width: 70%;
float: left;
}
#input-num-results-container,
#output-num-results-container {
width: 20%;
float: right;
margin: 0;
margin-left: 10%;
}
#input-find-options-checkboxes,
#output-find-options-checkboxes {
list-style: none;
padding: 0;
margin: auto;
overflow-x: auto;
overflow-y: hidden;
text-align: center;
width: fit-content;
}
#input-find-options-checkboxes li,
#output-find-options-checkboxes li {
display: flex;
flex-direction: row;
float: left;
padding: 10px;
text-align: center;
}
#input-search-results,
#output-search-results {
list-style: none;
width: 75%;
min-width: 200px;
margin-left: auto;
margin-right: auto;
}
#input-search-results li,
#output-search-results li {
padding-left: 5px;
padding-right: 5px;
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
width: 100%;
color: var(--op-list-operation-font-colour);
background-color: var(--op-list-operation-bg-colour);
border-bottom: 2px solid var(--op-list-operation-border-colour);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#input-search-results li:first-of-type,
#output-search-results li:first-of-type {
border-top: 2px solid var(--op-list-operation-border-colour);
}
#input-search-results li:hover,
#output-search-results li:hover {
cursor: pointer;
filter: brightness(98%);
}

View file

@ -77,3 +77,34 @@
padding: 20px;
border-left: 2px solid var(--primary-border-colour);
}
.checkbox label input[type=checkbox]+.checkbox-decorator .check,
.checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
border-color: var(--input-border-colour);
color: var(--input-highlight-colour);
}
.checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
.checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
border-color: var(--input-highlight-colour);
color: var(--input-highlight-colour);
}
.bmd-form-group.is-focused .option-item label {
color: var(--input-highlight-colour);
}
.bmd-form-group.is-focused [class^='bmd-label'],
.bmd-form-group.is-focused [class*=' bmd-label'],
.bmd-form-group.is-focused [class^='bmd-label'],
.bmd-form-group.is-focused [class*=' bmd-label'],
.bmd-form-group.is-focused label,
.checkbox label:hover {
color: var(--input-highlight-colour);
}
.bmd-form-group.option-item label+.form-control{
background-image:
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
}

View file

@ -16,7 +16,7 @@
padding-left: 10px;
padding-right: 10px;
background-image:
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
}
@ -33,7 +33,7 @@
}
#categories a {
color: #1976d2;
color: var(--category-list-font-colour);
cursor: pointer;
}

View file

@ -6,14 +6,14 @@
* @license Apache-2.0
*/
#loader-wrapper {
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: var(--secondary-border-colour);
background-color: var(--loader-background-colour);
}
.loader {
@ -26,7 +26,7 @@
margin: -75px 0 0 -75px;
border: 3px solid transparent;
border-top-color: #3498db;
border-top-color: var(--loader-outer-colour);
border-radius: 50%;
animation: spin 2s linear infinite;
@ -45,7 +45,7 @@
left: 5px;
right: 5px;
bottom: 5px;
border-top-color: #e74c3c;
border-top-color: var(--loader-middle-colour);
animation: spin 3s linear infinite;
}
@ -54,7 +54,7 @@
left: 13px;
right: 13px;
bottom: 13px;
border-top-color: #f9c922;
border-top-color: var(--loader-inner-colour);
animation: spin 1.5s linear infinite;
}

View file

@ -35,6 +35,14 @@
--banner-font-colour: #468847;
--banner-bg-colour: #dff0d8;
--banner-url-colour: #1976d2;
--category-list-font-colour: #1976d2;
--loader-background-colour: var(--secondary-border-colour);
--loader-outer-colour: #3498db;
--loader-middle-colour: #e74c3c;
--loader-inner-colour: #f9c922;
/* Operation colours */
@ -76,6 +84,13 @@
--arg-label-colour: #388e3c;
/* Operation buttons */
--disable-icon-colour: #9e9e9e;
--disable-icon-selected-colour: #f44336;
--breakpoint-icon-colour: #9e9e9e;
--breakpoint-icon-selected-colour: #f44336;
/* Buttons */
--btn-default-font-colour: #333;
--btn-default-bg-colour: #fff;
@ -114,4 +129,6 @@
--popover-border-colour: #ccc;
--code-background: #f9f2f4;
--code-font-colour: #c7254e;
--input-highlight-colour: #1976d2;
--input-border-colour: #424242;
}

View file

@ -31,6 +31,14 @@
--banner-font-colour: #c5c5c5;
--banner-bg-colour: #252525;
--banner-url-colour: #1976d2;
--category-list-font-colour: #1976d2;
--loader-background-colour: var(--secondary-border-colour);
--loader-outer-colour: #3498db;
--loader-middle-colour: #e74c3c;
--loader-inner-colour: #f9c922;
/* Operation colours */
@ -72,6 +80,13 @@
--arg-label-colour: rgb(25, 118, 210);
/* Operation buttons */
--disable-icon-colour: #9e9e9e;
--disable-icon-selected-colour: #f44336;
--breakpoint-icon-colour: #9e9e9e;
--breakpoint-icon-selected-colour: #f44336;
/* Buttons */
--btn-default-font-colour: #c5c5c5;
--btn-default-bg-colour: #2d2d2d;
@ -110,4 +125,6 @@
--popover-border-colour: #555;
--code-background: #0e639c;
--code-font-colour: #fff;
--input-highlight-colour: #1976d2;
--input-border-colour: #424242;
}

View file

@ -31,6 +31,14 @@
--banner-font-colour: white;
--banner-bg-colour: maroon;
--banner-url-colour: yellow;
--category-list-font-colour: yellow;
--loader-background-colour: #00f;
--loader-outer-colour: #0f0;
--loader-middle-colour: red;
--loader-inner-colour: yellow;
/* Operation colours */
@ -72,6 +80,13 @@
--arg-label-colour: red;
/* Operation buttons */
--disable-icon-colour: #0f0;
--disable-icon-selected-colour: yellow;
--breakpoint-icon-colour: #0f0;
--breakpoint-icon-selected-colour: yellow;
/* Buttons */
--btn-default-font-colour: black;
--btn-default-bg-colour: white;
@ -110,4 +125,6 @@
--popover-border-colour: violet;
--code-background: black;
--code-font-colour: limegreen;
--input-highlight-colour: limegreen;
--input-border-colour: limegreen;
}

View file

@ -0,0 +1,147 @@
/**
* Solarized dark theme definitions
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
:root.solarizedDark {
--base03: #002b36;
--base02: #073642;
--base01: #586e75;
--base00: #657b83;
--base0: #839496;
--base1: #93a1a1;
--base2: #eee8d5;
--base3: #fdf6e3;
--sol-yellow: #b58900;
--sol-orange: #cb4b16;
--sol-red: #dc322f;
--sol-magenta: #d33682;
--sol-violet: #6c71c4;
--sol-blue: #268bd2;
--sol-cyan: #2aa198;
--sol-green: #859900;
--primary-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
--primary-font-colour: var(--base0);
--primary-font-size: 14px;
--primary-line-height: 20px;
--fixed-width-font-family: SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--fixed-width-font-colour: inherit;
--fixed-width-font-size: inherit;
--subtext-font-colour: var(--base01);
--subtext-font-size: 13px;
--primary-background-colour: var(--base03);
--secondary-background-colour: var(--base02);
--primary-border-colour: var(--base00);
--secondary-border-colour: var(--base01);
--title-colour: var(--base1);
--title-weight: bold;
--title-background-colour: var(--base02);
--banner-font-colour: var(--base0);
--banner-bg-colour: var(--base03);
--banner-url-colour: var(--base1);
--category-list-font-colour: var(--base1);
--loader-background-colour: var(--base03);
--loader-outer-colour: var(--base1);
--loader-middle-colour: var(--base0);
--loader-inner-colour: var(--base00);
/* Operation colours */
--op-list-operation-font-colour: var(--base0);
--op-list-operation-bg-colour: var(--base03);
--op-list-operation-border-colour: var(--base02);
--rec-list-operation-font-colour: var(--base0);
--rec-list-operation-bg-colour: var(--base02);
--rec-list-operation-border-colour: var(--base01);
--selected-operation-font-color: var(--base1);
--selected-operation-bg-colour: var(--base02);
--selected-operation-border-colour: var(--base01);
--breakpoint-font-colour: var(--sol-red);
--breakpoint-bg-colour: var(--base02);
--breakpoint-border-colour: var(--base00);
--disabled-font-colour: var(--base01);
--disabled-bg-colour: var(--base03);
--disabled-border-colour: var(--base02);
--fc-operation-font-colour: var(--base1);
--fc-operation-bg-colour: var(--base02);
--fc-operation-border-colour: var(--base01);
--fc-breakpoint-operation-font-colour: var(--sol-orange);
--fc-breakpoint-operation-bg-colour: var(--base02);
--fc-breakpoint-operation-border-colour: var(--base00);
/* Operation arguments */
--op-title-font-weight: bold;
--arg-font-colour: var(--base0);
--arg-background: var(--base03);
--arg-border-colour: var(--base00);
--arg-disabled-background: var(--base03);
--arg-label-colour: var(--base1);
/* Operation buttons */
--disable-icon-colour: var(--base00);
--disable-icon-selected-colour: var(--sol-red);
--breakpoint-icon-colour: var(--base00);
--breakpoint-icon-selected-colour: var(--sol-red);
/* Buttons */
--btn-default-font-colour: var(--base0);
--btn-default-bg-colour: var(--base02);
--btn-default-border-colour: var(--base01);
--btn-default-hover-font-colour: var(--base1);
--btn-default-hover-bg-colour: var(--base01);
--btn-default-hover-border-colour: var(--base00);
--btn-success-font-colour: var(--base0);
--btn-success-bg-colour: var(--base03);
--btn-success-border-colour: var(--base00);
--btn-success-hover-font-colour: var(--base1);
--btn-success-hover-bg-colour: var(--base01);
--btn-success-hover-border-colour: var(--base00);
/* Highlighter colours */
--hl1: var(--base01);
--hl2: var(--sol-blue);
--hl3: var(--sol-magenta);
--hl4: var(--sol-yellow);
--hl5: var(--sol-green);
/* Scrollbar */
--scrollbar-track: var(--base03);
--scrollbar-thumb: var(--base00);
--scrollbar-hover: var(--base01);
/* Misc. */
--drop-file-border-colour: var(--base01);
--popover-background: var(--base02);
--popover-border-colour: var(--base01);
--code-background: var(--base03);
--code-font-colour: var(--base1);
--input-highlight-colour: var(--base1);
--input-border-colour: var(--base0);
}

View file

@ -0,0 +1,149 @@
/**
* Solarized light theme definitions
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
:root.solarizedLight {
--base03: #002b36;
--base02: #073642;
--base01: #586e75;
--base00: #657b83;
--base0: #839496;
--base1: #93a1a1;
--base2: #eee8d5;
--base3: #fdf6e3;
--sol-yellow: #b58900;
--sol-orange: #cb4b16;
--sol-red: #dc322f;
--sol-magenta: #d33682;
--sol-violet: #6c71c4;
--sol-blue: #268bd2;
--sol-cyan: #2aa198;
--sol-green: #859900;
--primary-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
--primary-font-colour: var(--base00);
--primary-font-size: 14px;
--primary-line-height: 20px;
--fixed-width-font-family: SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--fixed-width-font-colour: inherit;
--fixed-width-font-size: inherit;
--subtext-font-colour: var(--base1);
--subtext-font-size: 13px;
--primary-background-colour: var(--base3);
--secondary-background-colour: var(--base2);
--primary-border-colour: var(--base0);
--secondary-border-colour: var(--base1);
--title-colour: var(--base01);
--title-weight: bold;
--title-background-colour: var(--base2);
--banner-font-colour: var(--base00);
--banner-bg-colour: var(--base3);
--banner-url-colour: var(--base01);
--category-list-font-colour: var(--base01);
--loader-background-colour: var(--base3);
--loader-outer-colour: var(--base01);
--loader-middle-colour: var(--base00);
--loader-inner-colour: var(--base0);
/* Operation colours */
--op-list-operation-font-colour: var(--base00);
--op-list-operation-bg-colour: var(--base3);
--op-list-operation-border-colour: var(--base2);
--rec-list-operation-font-colour: var(--base00);
--rec-list-operation-bg-colour: var(--base2);
--rec-list-operation-border-colour: var(--base1);
--selected-operation-font-color: var(--base01);
--selected-operation-bg-colour: var(--base2);
--selected-operation-border-colour: var(--base1);
--breakpoint-font-colour: var(--sol-red);
--breakpoint-bg-colour: var(--base2);
--breakpoint-border-colour: var(--base0);
--disabled-font-colour: var(--base1);
--disabled-bg-colour: var(--base3);
--disabled-border-colour: var(--base2);
--fc-operation-font-colour: var(--base01);
--fc-operation-bg-colour: var(--base2);
--fc-operation-border-colour: var(--base1);
--fc-breakpoint-operation-font-colour: var(--base02);
--fc-breakpoint-operation-bg-colour: var(--base1);
--fc-breakpoint-operation-border-colour: var(--base0);
/* Operation arguments */
--op-title-font-weight: bold;
--arg-font-colour: var(--base00);
--arg-background: var(--base3);
--arg-border-colour: var(--base0);
--arg-disabled-background: var(--base3);
--arg-label-colour: var(--base01);
/* Operation buttons */
--disable-icon-colour: #9e9e9e;
--disable-icon-selected-colour: #f44336;
--breakpoint-icon-colour: #9e9e9e;
--breakpoint-icon-selected-colour: #f44336;
/* Buttons */
--btn-default-font-colour: var(--base00);
--btn-default-bg-colour: var(--base2);
--btn-default-border-colour: var(--base1);
--btn-default-hover-font-colour: var(--base01);
--btn-default-hover-bg-colour: var(--base1);
--btn-default-hover-border-colour: var(--base0);
--btn-success-font-colour: var(--base00);
--btn-success-bg-colour: var(--base3);
--btn-success-border-colour: var(--base0);
--btn-success-hover-font-colour: var(--base01);
--btn-success-hover-bg-colour: var(--base1);
--btn-success-hover-border-colour: var(--base0);
/* Highlighter colours */
--hl1: var(--base1);
--hl2: var(--sol-blue);
--hl3: var(--sol-magenta);
--hl4: var(--sol-yellow);
--hl5: var(--sol-green);
/* Scrollbar */
--scrollbar-track: var(--base3);
--scrollbar-thumb: var(--base1);
--scrollbar-hover: var(--base0);
/* Misc. */
--drop-file-border-colour: var(--base1);
--popover-background: var(--base2);
--popover-border-colour: var(--base1);
--code-background: var(--base3);
--code-font-colour: var(--base01);
--input-highlight-colour: var(--base01);
--input-border-colour: var(--base00);
}

View file

@ -104,8 +104,11 @@ select.form-control:not([size]):not([multiple]), select.custom-file-control:not(
color: var(--primary-font-colour);
}
.form-control {
background-image: linear-gradient(to top, rgb(25, 118, 210) 2px, rgba(25, 118, 210, 0) 2px), linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
.form-control,
.is-focused .form-control {
background-image:
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
}
code {

View file

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker";
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
/**
* Waiter to handle conversations with a ChefWorker in the background.
@ -68,6 +68,7 @@ class BackgroundWorkerWaiter {
break;
case "optionUpdate":
case "statusMessage":
case "progressMessage":
// Ignore these messages
break;
default:

View file

@ -98,11 +98,11 @@ class BindingsWaiter {
break;
case "Space": // Bake
e.preventDefault();
this.app.bake();
this.manager.controls.bakeClick();
break;
case "Quote": // Step through
e.preventDefault();
this.app.bake(true);
this.manager.controls.stepClick();
break;
case "KeyC": // Clear recipe
e.preventDefault();
@ -120,6 +120,22 @@ class BindingsWaiter {
e.preventDefault();
this.manager.output.switchClick();
break;
case "KeyT": // New tab
e.preventDefault();
this.manager.input.addInputClick();
break;
case "KeyW": // Close tab
e.preventDefault();
this.manager.input.removeInput(this.manager.tabs.getActiveInputTab());
break;
case "ArrowLeft": // Go to previous tab
e.preventDefault();
this.manager.input.changeTabLeft();
break;
case "ArrowRight": // Go to next tab
e.preventDefault();
this.manager.input.changeTabRight();
break;
default:
if (e.code.match(/Digit[0-9]/g)) { // Select nth operation
e.preventDefault();
@ -216,6 +232,26 @@ class BindingsWaiter {
<td>Ctrl+${modWinLin}+m</td>
<td>Ctrl+${modMac}+m</td>
</tr>
<tr>
<td>Create a new tab</td>
<td>Ctrl+${modWinLin}+t</td>
<td>Ctrl+${modMac}+t</td>
</tr>
<tr>
<td>Close the current tab</td>
<td>Ctrl+${modWinLin}+w</td>
<td>Ctrl+${modMac}+w</td>
</tr>
<tr>
<td>Go to next tab</td>
<td>Ctrl+${modWinLin}+RightArrow</td>
<td>Ctrl+${modMac}+RightArrow</td>
</tr>
<tr>
<td>Go to previous tab</td>
<td>Ctrl+${modWinLin}+LeftArrow</td>
<td>Ctrl+${modMac}+LeftArrow</td>
</tr>
`;
}

View file

@ -4,8 +4,7 @@
* @license Apache-2.0
*/
import Utils from "../core/Utils";
import {toBase64} from "../core/lib/Base64";
import Utils from "../../core/Utils";
/**
@ -57,10 +56,11 @@ class ControlsWaiter {
* Handler to trigger baking.
*/
bakeClick() {
if (document.getElementById("bake").textContent.indexOf("Bake") > 0) {
this.app.bake();
} else {
this.manager.worker.cancelBake();
const btnBake = document.getElementById("bake");
if (btnBake.textContent.indexOf("Bake") > 0) {
this.app.manager.input.bakeAll();
} else if (btnBake.textContent.indexOf("Cancel") > 0) {
this.manager.worker.cancelBake(false, true);
}
}
@ -69,7 +69,7 @@ class ControlsWaiter {
* Handler for the 'Step through' command. Executes the next step of the recipe.
*/
stepClick() {
this.app.bake(true);
this.app.step();
}
@ -90,7 +90,7 @@ class ControlsWaiter {
/**
* Populates the save disalog box with a URL incorporating the recipe and input.
* Populates the save dialog box with a URL incorporating the recipe and input.
*
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
*/
@ -112,26 +112,33 @@ class ControlsWaiter {
*
* @param {boolean} includeRecipe - Whether to include the recipe in the URL.
* @param {boolean} includeInput - Whether to include the input in the URL.
* @param {string} input
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
* @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
* @returns {string}
*/
generateStateUrl(includeRecipe, includeInput, recipeConfig, baseURL) {
generateStateUrl(includeRecipe, includeInput, input, recipeConfig, baseURL) {
recipeConfig = recipeConfig || this.app.getRecipeConfig();
const link = baseURL || window.location.protocol + "//" +
window.location.host +
window.location.pathname;
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
const inputStr = toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
includeRecipe = includeRecipe && (recipeConfig.length > 0);
// Only inlcude input if it is less than 50KB (51200 * 4/3 as it is Base64 encoded)
includeInput = includeInput && (inputStr.length > 0) && (inputStr.length <= 68267);
// If we don't get passed an input, get it from the current URI
if (input === null) {
const params = this.app.getURIParams();
if (params.input) {
includeInput = true;
input = params.input;
}
}
const params = [
includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput ? ["input", inputStr] : undefined,
includeInput ? ["input", input] : undefined,
];
const hash = params
@ -335,7 +342,7 @@ class ControlsWaiter {
e.preventDefault();
const reportBugInfo = document.getElementById("report-bug-info");
const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/");
if (reportBugInfo) {
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
@ -370,22 +377,34 @@ ${navigator.userAgent}
/**
* Switches the Bake button between 'Bake' and 'Cancel' functions.
* Switches the Bake button between 'Bake', 'Cancel' and 'Loading' functions.
*
* @param {boolean} cancel - Whether to change to cancel or not
* @param {string} func - The function to change to. Either "cancel", "loading" or "bake"
*/
toggleBakeButtonFunction(cancel) {
toggleBakeButtonFunction(func) {
const bakeButton = document.getElementById("bake"),
btnText = bakeButton.querySelector("span");
if (cancel) {
btnText.innerText = "Cancel";
bakeButton.classList.remove("btn-success");
bakeButton.classList.add("btn-danger");
} else {
btnText.innerText = "Bake!";
bakeButton.classList.remove("btn-danger");
bakeButton.classList.add("btn-success");
switch (func) {
case "cancel":
btnText.innerText = "Cancel";
bakeButton.classList.remove("btn-success");
bakeButton.classList.remove("btn-warning");
bakeButton.classList.add("btn-danger");
break;
case "loading":
bakeButton.style.background = "";
btnText.innerText = "Loading...";
bakeButton.classList.remove("btn-success");
bakeButton.classList.remove("btn-danger");
bakeButton.classList.add("btn-warning");
break;
default:
bakeButton.style.background = "";
btnText.innerText = "Bake!";
bakeButton.classList.remove("btn-danger");
bakeButton.classList.remove("btn-warning");
bakeButton.classList.add("btn-success");
}
}

View file

@ -378,6 +378,8 @@ class HighlighterWaiter {
displayHighlights(pos, direction) {
if (!pos) return;
if (this.manager.tabs.getActiveInputTab() !== this.manager.tabs.getActiveOutputTab()) return;
const io = direction === "forward" ? "output" : "input";
document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import HTMLOperation from "./HTMLOperation";
import HTMLOperation from "../HTMLOperation";
import Sortable from "sortablejs";

View file

@ -168,6 +168,7 @@ OptionsWaiter.prototype.logLevelChange = function (e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
this.manager.input.setLogLevel();
};
export default OptionsWaiter;

1417
src/web/waiters/OutputWaiter.mjs Executable file

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@
* @license Apache-2.0
*/
import HTMLOperation from "./HTMLOperation";
import HTMLOperation from "../HTMLOperation";
import Sortable from "sortablejs";
import Utils from "../core/Utils";
import Utils from "../../core/Utils";
/**

View file

@ -5,8 +5,8 @@
*/
import clippy from "clippyjs";
import "./static/clippy_assets/agents/Clippy/agent.js";
import clippyMap from "./static/clippy_assets/agents/Clippy/map.png";
import "../static/clippy_assets/agents/Clippy/agent.js";
import clippyMap from "../static/clippy_assets/agents/Clippy/map.png";
/**
* Waiter to handle seasonal events and easter eggs.

View file

@ -0,0 +1,428 @@
/**
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
/**
* Waiter to handle events related to the input and output tabs
*/
class TabWaiter {
/**
* TabWaiter constructor.
*
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
}
/**
* Calculates the maximum number of tabs to display
*
* @returns {number}
*/
calcMaxTabs() {
let numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120);
numTabs = (numTabs > 1) ? numTabs : 2;
return numTabs;
}
/**
* Gets the currently active input or active tab number
*
* @param {string} io - Either "input" or "output"
* @returns {number} - The currently active tab or -1
*/
getActiveTab(io) {
const activeTabs = document.getElementsByClassName(`active-${io}-tab`);
if (activeTabs.length > 0) {
if (!activeTabs.item(0).hasAttribute("inputNum")) return -1;
const tabNum = activeTabs.item(0).getAttribute("inputNum");
return parseInt(tabNum, 10);
}
return -1;
}
/**
* Gets the currently active input tab number
*
* @returns {number}
*/
getActiveInputTab() {
return this.getActiveTab("input");
}
/**
* Gets the currently active output tab number
*
* @returns {number}
*/
getActiveOutputTab() {
return this.getActiveTab("output");
}
/**
* Gets the li element for the tab of a given input number
*
* @param {number} inputNum - The inputNum of the tab we're trying to get
* @param {string} io - Either "input" or "output"
* @returns {Element}
*/
getTabItem(inputNum, io) {
const tabs = document.getElementById(`${io}-tabs`).children;
for (let i = 0; i < tabs.length; i++) {
if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
return tabs.item(i);
}
}
return null;
}
/**
* Gets the li element for an input tab of the given input number
*
* @param {inputNum} - The inputNum of the tab we're trying to get
* @returns {Element}
*/
getInputTabItem(inputNum) {
return this.getTabItem(inputNum, "input");
}
/**
* Gets the li element for an output tab of the given input number
*
* @param {number} inputNum
* @returns {Element}
*/
getOutputTabItem(inputNum) {
return this.getTabItem(inputNum, "output");
}
/**
* Gets a list of tab numbers for the currently displayed tabs
*
* @param {string} io - Either "input" or "output"
* @returns {number[]}
*/
getTabList(io) {
const nums = [],
tabs = document.getElementById(`${io}-tabs`).children;
for (let i = 0; i < tabs.length; i++) {
nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
}
return nums;
}
/**
* Gets a list of tab numbers for the currently displayed input tabs
*
* @returns {number[]}
*/
getInputTabList() {
return this.getTabList("input");
}
/**
* Gets a list of tab numbers for the currently displayed output tabs
*
* @returns {number[]}
*/
getOutputTabList() {
return this.getTabList("output");
}
/**
* Creates a new tab element for the tab bar
*
* @param {number} inputNum - The inputNum of the new tab
* @param {boolean} active - If true, sets the tab to active
* @param {string} io - Either "input" or "output"
* @returns {Element}
*/
createTabElement(inputNum, active, io) {
const newTab = document.createElement("li");
newTab.setAttribute("inputNum", inputNum.toString());
if (active) newTab.classList.add(`active-${io}-tab`);
const newTabContent = document.createElement("div");
newTabContent.classList.add(`${io}-tab-content`);
newTabContent.innerText = `Tab ${inputNum.toString()}`;
newTabContent.addEventListener("wheel", this.manager[io].scrollTab.bind(this.manager[io]), {passive: false});
newTab.appendChild(newTabContent);
if (io === "input") {
const newTabButton = document.createElement("button"),
newTabButtonIcon = document.createElement("i");
newTabButton.type = "button";
newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
newTabButtonIcon.classList.add("material-icons");
newTabButtonIcon.innerText = "clear";
newTabButton.appendChild(newTabButtonIcon);
newTabButton.addEventListener("click", this.manager.input.removeTabClick.bind(this.manager.input));
newTab.appendChild(newTabButton);
}
return newTab;
}
/**
* Creates a new tab element for the input tab bar
*
* @param {number} inputNum - The inputNum of the new input tab
* @param {boolean} [active=false] - If true, sets the tab to active
* @returns {Element}
*/
createInputTabElement(inputNum, active=false) {
return this.createTabElement(inputNum, active, "input");
}
/**
* Creates a new tab element for the output tab bar
*
* @param {number} inputNum - The inputNum of the new output tab
* @param {boolean} [active=false] - If true, sets the tab to active
* @returns {Element}
*/
createOutputTabElement(inputNum, active=false) {
return this.createTabElement(inputNum, active, "output");
}
/**
* Displays the tab bar for both the input and output
*/
showTabBar() {
document.getElementById("input-tabs-wrapper").style.display = "block";
document.getElementById("output-tabs-wrapper").style.display = "block";
document.getElementById("input-wrapper").classList.add("show-tabs");
document.getElementById("output-wrapper").classList.add("show-tabs");
document.getElementById("save-all-to-file").style.display = "inline-block";
}
/**
* Hides the tab bar for both the input and output
*/
hideTabBar() {
document.getElementById("input-tabs-wrapper").style.display = "none";
document.getElementById("output-tabs-wrapper").style.display = "none";
document.getElementById("input-wrapper").classList.remove("show-tabs");
document.getElementById("output-wrapper").classList.remove("show-tabs");
document.getElementById("save-all-to-file").style.display = "none";
}
/**
* Redraws the tab bar with an updated list of tabs, then changes to activeTab
*
* @param {number[]} nums - The inputNums of the tab bar to be drawn
* @param {number} activeTab - The inputNum of the activeTab
* @param {boolean} tabsLeft - True if there are tabs to the left of the displayed tabs
* @param {boolean} tabsRight - True if there are tabs to the right of the displayed tabs
* @param {string} io - Either "input" or "output"
*/
refreshTabs(nums, activeTab, tabsLeft, tabsRight, io) {
const tabsList = document.getElementById(`${io}-tabs`);
// Remove existing tab elements
for (let i = tabsList.children.length - 1; i >= 0; i--) {
tabsList.children.item(i).remove();
}
// Create and add new tab elements
for (let i = 0; i < nums.length; i++) {
const active = (nums[i] === activeTab);
tabsList.appendChild(this.createTabElement(nums[i], active, io));
}
// Display shadows if there are tabs left / right of the displayed tabs
if (tabsLeft) {
tabsList.classList.add("tabs-left");
} else {
tabsList.classList.remove("tabs-left");
}
if (tabsRight) {
tabsList.classList.add("tabs-right");
} else {
tabsList.classList.remove("tabs-right");
}
// Show or hide the tab bar depending on how many tabs we have
if (nums.length > 1) {
this.showTabBar();
} else {
this.hideTabBar();
}
}
/**
* Refreshes the input tabs, and changes to activeTab
*
* @param {number[]} nums - The inputNums to be displayed as tabs
* @param {number} activeTab - The tab to change to
* @param {boolean} tabsLeft - True if there are input tabs to the left of the displayed tabs
* @param {boolean} tabsRight - True if there are input tabs to the right of the displayed tabs
*/
refreshInputTabs(nums, activeTab, tabsLeft, tabsRight) {
this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "input");
}
/**
* Refreshes the output tabs, and changes to activeTab
*
* @param {number[]} nums - The inputNums to be displayed as tabs
* @param {number} activeTab - The tab to change to
* @param {boolean} tabsLeft - True if there are output tabs to the left of the displayed tabs
* @param {boolean} tabsRight - True if there are output tabs to the right of the displayed tabs
*/
refreshOutputTabs(nums, activeTab, tabsLeft, tabsRight) {
this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "output");
}
/**
* Changes the active tab to a different tab
*
* @param {number} inputNum - The inputNum of the tab to change to
* @param {string} io - Either "input" or "output"
* @return {boolean} - False if the tab is not currently being displayed
*/
changeTab(inputNum, io) {
const tabsList = document.getElementById(`${io}-tabs`);
this.manager.highlighter.removeHighlights();
getSelection().removeAllRanges();
let found = false;
for (let i = 0; i < tabsList.children.length; i++) {
const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
if (tabNum === inputNum) {
tabsList.children.item(i).classList.add(`active-${io}-tab`);
found = true;
} else {
tabsList.children.item(i).classList.remove(`active-${io}-tab`);
}
}
return found;
}
/**
* Changes the active input tab to a different tab
*
* @param {number} inputNum
* @returns {boolean} - False if the tab is not currently being displayed
*/
changeInputTab(inputNum) {
return this.changeTab(inputNum, "input");
}
/**
* Changes the active output tab to a different tab
*
* @param {number} inputNum
* @returns {boolean} - False if the tab is not currently being displayed
*/
changeOutputTab(inputNum) {
return this.changeTab(inputNum, "output");
}
/**
* Updates the tab header to display a preview of the tab contents
*
* @param {number} inputNum - The inputNum of the tab to update the header of
* @param {string} data - The data to display in the tab header
* @param {string} io - Either "input" or "output"
*/
updateTabHeader(inputNum, data, io) {
const tab = this.getTabItem(inputNum, io);
if (tab === null) return;
let headerData = `Tab ${inputNum}`;
if (data.length > 0) {
headerData = data.slice(0, 100);
headerData = `${inputNum}: ${headerData}`;
}
tab.firstElementChild.innerText = headerData;
}
/**
* Updates the input tab header to display a preview of the tab contents
*
* @param {number} inputNum - The inputNum of the tab to update the header of
* @param {string} data - The data to display in the tab header
*/
updateInputTabHeader(inputNum, data) {
this.updateTabHeader(inputNum, data, "input");
}
/**
* Updates the output tab header to display a preview of the tab contents
*
* @param {number} inputNum - The inputNum of the tab to update the header of
* @param {string} data - The data to display in the tab header
*/
updateOutputTabHeader(inputNum, data) {
this.updateTabHeader(inputNum, data, "output");
}
/**
* Updates the tab background to display the progress of the current tab
*
* @param {number} inputNum - The inputNum of the tab
* @param {number} progress - The current progress
* @param {number} total - The total which the progress is a percent of
* @param {string} io - Either "input" or "output"
*/
updateTabProgress(inputNum, progress, total, io) {
const tabItem = this.getTabItem(inputNum, io);
if (tabItem === null) return;
const percentComplete = (progress / total) * 100;
if (percentComplete >= 100 || progress === false) {
tabItem.style.background = "";
} else {
tabItem.style.background = `linear-gradient(to right, var(--title-background-colour) ${percentComplete}%, var(--primary-background-colour) ${percentComplete}%)`;
}
}
/**
* Updates the input tab background to display its progress
*
* @param {number} inputNum
* @param {number} progress
* @param {number} total
*/
updateInputTabProgress(inputNum, progress, total) {
this.updateTabProgress(inputNum, progress, total, "input");
}
/**
* Updates the output tab background to display its progress
*
* @param {number} inputNum
* @param {number} progress
* @param {number} total
*/
updateOutputTabProgress(inputNum, progress, total) {
this.updateTabProgress(inputNum, progress, total, "output");
}
}
export default TabWaiter;

View file

@ -25,8 +25,7 @@ class WindowWaiter {
* continuous resetting).
*/
windowResize() {
clearTimeout(this.resetLayoutTimeout);
this.resetLayoutTimeout = setTimeout(this.app.resetLayout.bind(this.app), 200);
this.app.debounce(this.app.resetLayout, 200, "windowResize", this.app, [])();
}

View file

@ -0,0 +1,817 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
import DishWorker from "worker-loader?inline&fallback=false!../workers/DishWorker";
/**
* Waiter to handle conversations with the ChefWorker
*/
class WorkerWaiter {
/**
* WorkerWaiter constructor
*
* @param {App} app - The main view object for CyberChef
* @param {Manager} manager - The CyberChef event manager
*/
constructor(app, manager) {
this.app = app;
this.manager = manager;
this.loaded = false;
this.chefWorkers = [];
this.inputs = [];
this.inputNums = [];
this.totalOutputs = 0;
this.loadingOutputs = 0;
this.bakeId = 0;
this.callbacks = {};
this.callbackID = 0;
this.maxWorkers = 1;
if (navigator.hardwareConcurrency !== undefined &&
navigator.hardwareConcurrency > 1) {
this.maxWorkers = navigator.hardwareConcurrency - 1;
}
// Store dishWorker action (getDishAs or getDishTitle)
this.dishWorker = {
worker: null,
currentAction: ""
};
this.dishWorkerQueue = [];
}
/**
* Terminates any existing ChefWorkers and sets up a new worker
*/
setupChefWorker() {
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
this.removeChefWorker(this.chefWorkers[i]);
}
this.addChefWorker();
this.setupDishWorker();
}
/**
* Sets up a DishWorker to be used for performing Dish operations
*/
setupDishWorker() {
if (this.dishWorker.worker !== null) {
this.dishWorker.worker.terminate();
this.dishWorker.currentAction = "";
}
log.debug("Adding new DishWorker");
this.dishWorker.worker = new DishWorker();
this.dishWorker.worker.addEventListener("message", this.handleDishMessage.bind(this));
if (this.dishWorkerQueue.length > 0) {
this.postDishMessage(this.dishWorkerQueue.splice(0, 1)[0]);
}
}
/**
* Adds a new ChefWorker
*
* @returns {number} The index of the created worker
*/
addChefWorker() {
if (this.chefWorkers.length === this.maxWorkers) {
// Can't create any more workers
return -1;
}
log.debug("Adding new ChefWorker");
// Create a new ChefWorker and send it the docURL
const newWorker = new ChefWorker();
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
let docURL = document.location.href.split(/[#?]/)[0];
const index = docURL.lastIndexOf("/");
if (index > 0) {
docURL = docURL.substring(0, index);
}
newWorker.postMessage({"action": "docURL", "data": docURL});
newWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
});
// Store the worker, whether or not it's active, and the inputNum as an object
const newWorkerObj = {
worker: newWorker,
active: false,
inputNum: -1
};
this.chefWorkers.push(newWorkerObj);
return this.chefWorkers.indexOf(newWorkerObj);
}
/**
* Gets an inactive ChefWorker to be used for baking
*
* @param {boolean} [setActive=true] - If true, set the worker status to active
* @returns {number} - The index of the ChefWorker
*/
getInactiveChefWorker(setActive=true) {
for (let i = 0; i < this.chefWorkers.length; i++) {
if (!this.chefWorkers[i].active) {
this.chefWorkers[i].active = setActive;
return i;
}
}
return -1;
}
/**
* Removes a ChefWorker
*
* @param {Object} workerObj
*/
removeChefWorker(workerObj) {
const index = this.chefWorkers.indexOf(workerObj);
if (index === -1) {
return;
}
if (this.chefWorkers.length > 1 || this.chefWorkers[index].active) {
log.debug(`Removing ChefWorker at index ${index}`);
this.chefWorkers[index].worker.terminate();
this.chefWorkers.splice(index, 1);
}
// There should always be a ChefWorker loaded
if (this.chefWorkers.length === 0) {
this.addChefWorker();
}
}
/**
* Finds and returns the object for the ChefWorker of a given inputNum
*
* @param {number} inputNum
*/
getChefWorker(inputNum) {
for (let i = 0; i < this.chefWorkers.length; i++) {
if (this.chefWorkers[i].inputNum === inputNum) {
return this.chefWorkers[i];
}
}
}
/**
* Handler for messages sent back by the ChefWorkers
*
* @param {MessageEvent} e
*/
handleChefMessage(e) {
const r = e.data;
let inputNum = 0;
log.debug(`Receiving ${r.action} from ChefWorker.`);
if (r.data.hasOwnProperty("inputNum")) {
inputNum = r.data.inputNum;
}
const currentWorker = this.getChefWorker(inputNum);
switch (r.action) {
case "bakeComplete":
log.debug(`Bake ${inputNum} complete.`);
if (r.data.error) {
this.app.handleError(r.data.error);
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
} else {
this.updateOutput(r.data, r.data.inputNum, r.data.bakeId, r.data.progress);
}
this.app.progress = r.data.progress;
if (r.data.progress === this.recipeConfig.length) {
this.step = false;
}
this.workerFinished(currentWorker);
break;
case "bakeError":
this.app.handleError(r.data.error);
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
this.app.progress = r.data.progress;
this.workerFinished(currentWorker);
break;
case "dishReturned":
this.callbacks[r.data.id](r.data);
break;
case "silentBakeComplete":
break;
case "workerLoaded":
this.app.workerLoaded = true;
log.debug("ChefWorker loaded.");
if (!this.loaded) {
this.app.loaded();
this.loaded = true;
} else {
this.bakeNextInput(this.getInactiveChefWorker(false));
}
break;
case "statusMessage":
this.manager.output.updateOutputMessage(r.data.message, r.data.inputNum, true);
break;
case "progressMessage":
this.manager.output.updateOutputProgress(r.data.progress, r.data.total, r.data.inputNum);
break;
case "optionUpdate":
log.debug(`Setting ${r.data.option} to ${r.data.value}`);
this.app.options[r.data.option] = r.data.value;
break;
case "setRegisters":
this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers);
break;
case "highlightsCalculated":
this.manager.highlighter.displayHighlights(r.data.pos, r.data.direction);
break;
default:
log.error("Unrecognised message from ChefWorker", e);
break;
}
}
/**
* Update the value of an output
*
* @param {Object} data
* @param {number} inputNum
* @param {number} bakeId
* @param {number} progress
*/
updateOutput(data, inputNum, bakeId, progress) {
this.manager.output.updateOutputBakeId(bakeId, inputNum);
if (progress === this.recipeConfig.length) {
progress = false;
}
this.manager.output.updateOutputProgress(progress, this.recipeConfig.length, inputNum);
this.manager.output.updateOutputValue(data, inputNum, false);
if (progress !== false) {
this.manager.output.updateOutputStatus("error", inputNum);
if (inputNum === this.manager.tabs.getActiveInputTab()) {
this.manager.recipe.updateBreakpointIndicator(progress);
}
} else {
this.manager.output.updateOutputStatus("baked", inputNum);
}
}
/**
* Updates the UI to show if baking is in progress or not.
*
* @param {boolean} bakingStatus
*/
setBakingStatus(bakingStatus) {
this.app.baking = bakingStatus;
this.app.debounce(this.manager.controls.toggleBakeButtonFunction, 20, "toggleBakeButton", this, [bakingStatus ? "cancel" : "bake"])();
if (bakingStatus) this.manager.output.hideMagicButton();
}
/**
* Get the progress of the ChefWorkers
*/
getBakeProgress() {
const pendingInputs = this.inputNums.length + this.loadingOutputs + this.inputs.length;
let bakingInputs = 0;
for (let i = 0; i < this.chefWorkers.length; i++) {
if (this.chefWorkers[i].active) {
bakingInputs++;
}
}
const total = this.totalOutputs;
const bakedInputs = total - pendingInputs - bakingInputs;
return {
total: total,
pending: pendingInputs,
baking: bakingInputs,
baked: bakedInputs
};
}
/**
* Cancels the current bake by terminating and removing all ChefWorkers
*
* @param {boolean} [silent=false] - If true, don't set the output
* @param {boolean} killAll - If true, kills all chefWorkers regardless of status
*/
cancelBake(silent, killAll) {
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
if (this.chefWorkers[i].active || killAll) {
const inputNum = this.chefWorkers[i].inputNum;
this.removeChefWorker(this.chefWorkers[i]);
this.manager.output.updateOutputStatus("inactive", inputNum);
}
}
this.setBakingStatus(false);
for (let i = 0; i < this.inputs.length; i++) {
this.manager.output.updateOutputStatus("inactive", this.inputs[i].inputNum);
}
for (let i = 0; i < this.inputNums.length; i++) {
this.manager.output.updateOutputStatus("inactive", this.inputNums[i]);
}
const tabList = this.manager.tabs.getOutputTabList();
for (let i = 0; i < tabList.length; i++) {
this.manager.tabs.getOutputTabItem(tabList[i]).style.background = "";
}
this.inputs = [];
this.inputNums = [];
this.totalOutputs = 0;
this.loadingOutputs = 0;
if (!silent) this.manager.output.set(this.manager.tabs.getActiveOutputTab());
}
/**
* Handle a worker completing baking
*
* @param {object} workerObj - Object containing the worker information
* @param {ChefWorker} workerObj.worker - The actual worker object
* @param {number} workerObj.inputNum - The inputNum of the input being baked by the worker
* @param {boolean} workerObj.active - If true, the worker is currrently baking an input
*/
workerFinished(workerObj) {
const workerIdx = this.chefWorkers.indexOf(workerObj);
this.chefWorkers[workerIdx].active = false;
if (this.inputs.length > 0) {
this.bakeNextInput(workerIdx);
} else if (this.inputNums.length === 0 && this.loadingOutputs === 0) {
// The ChefWorker is no longer needed
log.debug("No more inputs to bake.");
const progress = this.getBakeProgress();
if (progress.total === progress.baked) {
this.bakingComplete();
}
}
}
/**
* Handler for completed bakes
*/
bakingComplete() {
this.setBakingStatus(false);
let duration = new Date().getTime() - this.bakeStartTime;
duration = duration.toLocaleString() + "ms";
const progress = this.getBakeProgress();
if (progress.total > 1) {
let width = progress.total.toLocaleString().length;
if (duration.length > width) {
width = duration.length;
}
width = width < 2 ? 2 : width;
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, "&nbsp;");
const durationStr = duration.padStart(width, " ").replace(/ /g, "&nbsp;");
const inputNums = Object.keys(this.manager.output.outputs);
let avgTime = 0,
numOutputs = 0;
for (let i = 0; i < inputNums.length; i++) {
const output = this.manager.output.outputs[inputNums[i]];
if (output.status === "baked") {
numOutputs++;
avgTime += output.data.duration;
}
}
avgTime = Math.round(avgTime / numOutputs).toLocaleString() + "ms";
avgTime = avgTime.padStart(width, " ").replace(/ /g, "&nbsp;");
const msg = `total: ${totalStr}<br>time: ${durationStr}<br>average: ${avgTime}`;
const bakeInfo = document.getElementById("bake-info");
bakeInfo.innerHTML = msg;
bakeInfo.style.display = "";
} else {
document.getElementById("bake-info").style.display = "none";
}
document.getElementById("bake").style.background = "";
this.totalOutputs = 0; // Reset for next time
log.debug("--- Bake complete ---");
}
/**
* Bakes the next input and tells the inputWorker to load the next input
*
* @param {number} workerIdx - The index of the worker to bake with
*/
bakeNextInput(workerIdx) {
if (this.inputs.length === 0) return;
if (workerIdx === -1) return;
if (!this.chefWorkers[workerIdx]) return;
this.chefWorkers[workerIdx].active = true;
const nextInput = this.inputs.splice(0, 1)[0];
if (typeof nextInput.inputNum === "string") nextInput.inputNum = parseInt(nextInput.inputNum, 10);
log.debug(`Baking input ${nextInput.inputNum}.`);
this.manager.output.updateOutputMessage(`Baking input ${nextInput.inputNum}...`, nextInput.inputNum, false);
this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
this.chefWorkers[workerIdx].inputNum = nextInput.inputNum;
const input = nextInput.input,
recipeConfig = this.recipeConfig;
if (this.step) {
// Remove all breakpoints from the recipe up to progress
if (nextInput.progress !== false) {
for (let i = 0; i < nextInput.progress; i++) {
if (recipeConfig[i].hasOwnProperty("breakpoint")) {
delete recipeConfig[i].breakpoint;
}
}
}
// Set a breakpoint at the next operation so we stop baking there
if (recipeConfig[this.app.progress]) recipeConfig[this.app.progress].breakpoint = true;
}
let transferable;
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
transferable = [input];
}
this.chefWorkers[workerIdx].worker.postMessage({
action: "bake",
data: {
input: input,
recipeConfig: recipeConfig,
options: this.options,
inputNum: nextInput.inputNum,
bakeId: this.bakeId
}
}, transferable);
if (this.inputNums.length > 0) {
this.manager.input.inputWorker.postMessage({
action: "bakeNext",
data: {
inputNum: this.inputNums.splice(0, 1)[0],
bakeId: this.bakeId
}
});
this.loadingOutputs++;
}
}
/**
* Bakes the current input using the current recipe.
*
* @param {Object[]} recipeConfig
* @param {Object} options
* @param {number} progress
* @param {boolean} step
*/
bake(recipeConfig, options, progress, step) {
this.setBakingStatus(true);
this.manager.recipe.updateBreakpointIndicator(false);
this.bakeStartTime = new Date().getTime();
this.bakeId++;
this.recipeConfig = recipeConfig;
this.options = options;
this.progress = progress;
this.step = step;
this.displayProgress();
}
/**
* Queues an input ready to be baked
*
* @param {object} inputData
* @param {string | ArrayBuffer} inputData.input
* @param {number} inputData.inputNum
* @param {number} inputData.bakeId
*/
queueInput(inputData) {
this.loadingOutputs--;
if (this.app.baking && inputData.bakeId === this.bakeId) {
this.inputs.push(inputData);
this.bakeNextInput(this.getInactiveChefWorker(true));
}
}
/**
* Handles if an error is thrown by QueueInput
*
* @param {object} inputData
* @param {number} inputData.inputNum
* @param {number} inputData.bakeId
*/
queueInputError(inputData) {
this.loadingOutputs--;
if (this.app.baking && inputData.bakeId === this.bakeId) {
this.manager.output.updateOutputError("Error queueing the input for a bake.", inputData.inputNum, 0);
if (this.inputNums.length === 0) return;
// Load the next input
this.manager.input.inputWorker.postMessage({
action: "bakeNext",
data: {
inputNum: this.inputNums.splice(0, 1)[0],
bakeId: this.bakeId
}
});
this.loadingOutputs++;
}
}
/**
* Queues a list of inputNums to be baked by ChefWorkers, and begins baking
*
* @param {object} inputData
* @param {number[]} inputData.nums - The inputNums to be queued for baking
* @param {boolean} inputData.step - If true, only execute the next operation in the recipe
* @param {number} inputData.progress - The current progress through the recipe. Used when stepping
*/
async bakeAllInputs(inputData) {
return await new Promise(resolve => {
if (this.app.baking) return;
const inputNums = inputData.nums;
const step = inputData.step;
// Use cancelBake to clear out the inputs
this.cancelBake(true, false);
this.inputNums = inputNums;
this.totalOutputs = inputNums.length;
this.app.progress = inputData.progress;
let inactiveWorkers = 0;
for (let i = 0; i < this.chefWorkers.length; i++) {
if (!this.chefWorkers[i].active) {
inactiveWorkers++;
}
}
for (let i = 0; i < inputNums.length - inactiveWorkers; i++) {
if (this.addChefWorker() === -1) break;
}
this.app.bake(step);
for (let i = 0; i < this.inputNums.length; i++) {
this.manager.output.updateOutputMessage(`Input ${inputNums[i]} has not been baked yet.`, inputNums[i], false);
this.manager.output.updateOutputStatus("pending", inputNums[i]);
}
let numBakes = this.chefWorkers.length;
if (this.inputNums.length < numBakes) {
numBakes = this.inputNums.length;
}
for (let i = 0; i < numBakes; i++) {
this.manager.input.inputWorker.postMessage({
action: "bakeNext",
data: {
inputNum: this.inputNums.splice(0, 1)[0],
bakeId: this.bakeId
}
});
this.loadingOutputs++;
}
});
}
/**
* Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant
* JavaScript code needed to do a real bake.
*
* @param {Object[]} [recipeConfig]
*/
silentBake(recipeConfig) {
// If there aren't any active ChefWorkers, try to add one
let workerId = this.getInactiveChefWorker();
if (workerId === -1) {
workerId = this.addChefWorker();
}
if (workerId === -1) return;
this.chefWorkers[workerId].worker.postMessage({
action: "silentBake",
data: {
recipeConfig: recipeConfig
}
});
}
/**
* Handler for messages sent back from DishWorker
*
* @param {MessageEvent} e
*/
handleDishMessage(e) {
const r = e.data;
log.debug(`Receiving ${r.action} from DishWorker`);
switch (r.action) {
case "dishReturned":
this.dishWorker.currentAction = "";
this.callbacks[r.data.id](r.data);
if (this.dishWorkerQueue.length > 0) {
this.postDishMessage(this.dishWorkerQueue.splice(0, 1)[0]);
}
break;
default:
log.error("Unrecognised message from DishWorker", e);
break;
}
}
/**
* Asks the ChefWorker to return the dish as the specified type
*
* @param {Dish} dish
* @param {string} type
* @param {Function} callback
*/
getDishAs(dish, type, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
if (this.dishWorker.worker === null) this.setupDishWorker();
this.postDishMessage({
action: "getDishAs",
data: {
dish: dish,
type: type,
id: id
}
});
}
/**
* Asks the ChefWorker to get the title of the dish
*
* @param {Dish} dish
* @param {number} maxLength
* @param {Function} callback
* @returns {string}
*/
getDishTitle(dish, maxLength, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
if (this.dishWorker.worker === null) this.setupDishWorker();
this.postDishMessage({
action: "getDishTitle",
data: {
dish: dish,
maxLength: maxLength,
id: id
}
});
}
/**
* Queues a message to be sent to the dishWorker
*
* @param {object} message
* @param {string} message.action
* @param {object} message.data
* @param {Dish} message.data.dish
* @param {number} message.data.id
*/
queueDishMessage(message) {
if (message.action === "getDishAs") {
this.dishWorkerQueue = [message].concat(this.dishWorkerQueue);
} else {
this.dishWorkerQueue.push(message);
}
}
/**
* Sends a message to the DishWorker
*
* @param {object} message
* @param {string} message.action
* @param {object} message.data
*/
postDishMessage(message) {
if (this.dishWorker.currentAction !== "") {
this.queueDishMessage(message);
} else {
this.dishWorker.currentAction = message.action;
this.dishWorker.worker.postMessage(message);
}
}
/**
* Sets the console log level in the workers.
*/
setLogLevel() {
for (let i = 0; i < this.chefWorkers.length; i++) {
this.chefWorkers[i].worker.postMessage({
action: "setLogLevel",
data: log.getLevel()
});
}
}
/**
* Display the bake progress in the output bar and bake button
*/
displayProgress() {
const progress = this.getBakeProgress();
if (progress.total === progress.baked) return;
const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100;
const bakeButton = document.getElementById("bake");
if (this.app.baking) {
if (percentComplete < 100) {
bakeButton.style.background = `linear-gradient(to left, #fea79a ${percentComplete}%, #f44336 ${percentComplete}%)`;
} else {
bakeButton.style.background = "";
}
} else {
// not baking
bakeButton.style.background = "";
}
const bakeInfo = document.getElementById("bake-info");
if (progress.total > 1) {
let width = progress.total.toLocaleString().length;
width = width < 2 ? 2 : width;
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, "&nbsp;");
const bakedStr = progress.baked.toLocaleString().padStart(width, " ").replace(/ /g, "&nbsp;");
const pendingStr = progress.pending.toLocaleString().padStart(width, " ").replace(/ /g, "&nbsp;");
const bakingStr = progress.baking.toLocaleString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "total: " + totalStr;
msg += "<br>baked: " + bakedStr;
if (progress.pending > 0) {
msg += "<br>pending: " + pendingStr;
} else if (progress.baking > 0) {
msg += "<br>baking: " + bakingStr;
}
bakeInfo.innerHTML = msg;
bakeInfo.style.display = "";
} else {
bakeInfo.style.display = "none";
}
if (progress.total !== progress.baked) {
setTimeout(function() {
this.displayProgress();
}.bind(this), 100);
}
}
/**
* Asks the ChefWorker to calculate highlight offsets if possible.
*
* @param {Object[]} recipeConfig
* @param {string} direction
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
highlight(recipeConfig, direction, pos) {
let workerIdx = this.getInactiveChefWorker(false);
if (workerIdx === -1) {
workerIdx = this.addChefWorker();
}
if (workerIdx === -1) return;
this.chefWorkers[workerIdx].worker.postMessage({
action: "highlight",
data: {
recipeConfig: recipeConfig,
direction: direction,
pos: pos
}
});
}
}
export default WorkerWaiter;

View file

@ -0,0 +1,69 @@
/**
* Web worker to handle dish conversion operations.
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Dish from "../../core/Dish";
self.addEventListener("message", function(e) {
// Handle message from the main thread
const r = e.data;
log.debug(`DishWorker receiving command '${r.action}'`);
switch (r.action) {
case "getDishAs":
getDishAs(r.data);
break;
case "getDishTitle":
getDishTitle(r.data);
break;
default:
log.error(`DishWorker sent invalid action: '${r.action}'`);
}
});
/**
* Translates the dish to a given type
*
* @param {object} data
* @param {Dish} data.dish
* @param {string} data.type
* @param {number} data.id
*/
async function getDishAs(data) {
const newDish = new Dish(data.dish),
value = await newDish.get(data.type),
transferable = (data.type === "ArrayBuffer") ? [value] : undefined;
self.postMessage({
action: "dishReturned",
data: {
value: value,
id: data.id
}
}, transferable);
}
/**
* Gets the title of the given dish
*
* @param {object} data
* @param {Dish} data.dish
* @param {number} data.id
* @param {number} data.maxLength
*/
async function getDishTitle(data) {
const newDish = new Dish(data.dish),
title = await newDish.getTitle(data.maxLength);
self.postMessage({
action: "dishReturned",
data: {
value: title,
id: data.id
}
});
}

File diff suppressed because it is too large Load diff

View file

@ -6,14 +6,32 @@
* @license Apache-2.0
*/
self.id = null;
self.handleMessage = function(e) {
const r = e.data;
log.debug(`LoaderWorker receiving command '${r.action}'`);
switch (r.action) {
case "loadInput":
self.loadFile(r.data.file, r.data.inputNum);
break;
}
};
/**
* Respond to message from parent thread.
*/
self.addEventListener("message", function(e) {
const r = e.data;
if (r.hasOwnProperty("file")) {
self.loadFile(r.file);
if (r.hasOwnProperty("file") && (r.hasOwnProperty("inputNum"))) {
self.loadFile(r.file, r.inputNum);
} else if (r.hasOwnProperty("file")) {
self.loadFile(r.file, "");
} else if (r.hasOwnProperty("id")) {
self.id = r.id;
}
});
@ -22,20 +40,24 @@ self.addEventListener("message", function(e) {
* Loads a file object into an ArrayBuffer, then transfers it back to the parent thread.
*
* @param {File} file
* @param {string} inputNum
*/
self.loadFile = function(file) {
self.loadFile = function(file, inputNum) {
const reader = new FileReader();
if (file.size >= 256*256*256*128) {
self.postMessage({"error": "File size too large.", "inputNum": inputNum, "id": self.id});
return;
}
const data = new Uint8Array(file.size);
let offset = 0;
const CHUNK_SIZE = 10485760; // 10MiB
const seek = function() {
if (offset >= file.size) {
self.postMessage({"progress": 100});
self.postMessage({"fileBuffer": data.buffer}, [data.buffer]);
self.postMessage({"fileBuffer": data.buffer, "inputNum": inputNum, "id": self.id}, [data.buffer]);
return;
}
self.postMessage({"progress": Math.round(offset / file.size * 100)});
self.postMessage({"progress": Math.round(offset / file.size * 100), "inputNum": inputNum});
const slice = file.slice(offset, offset + CHUNK_SIZE);
reader.readAsArrayBuffer(slice);
};
@ -47,7 +69,7 @@ self.loadFile = function(file) {
};
reader.onerror = function(e) {
self.postMessage({"error": reader.error.message});
self.postMessage({"error": reader.error.message, "inputNum": inputNum, "id": self.id});
};
seek();

View file

@ -0,0 +1,73 @@
/**
* Web Worker to handle zipping the outputs for download.
*
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import zip from "zlibjs/bin/zip.min";
import Utils from "../../core/Utils";
import Dish from "../../core/Dish";
import {detectFileType} from "../../core/lib/FileType";
const Zlib = zip.Zlib;
/**
* Respond to message from parent thread.
*/
self.addEventListener("message", function(e) {
const r = e.data;
if (!r.hasOwnProperty("outputs")) {
log.error("No files were passed to the ZipWorker.");
return;
}
if (!r.hasOwnProperty("filename")) {
log.error("No filename was passed to the ZipWorker");
return;
}
self.zipFiles(r.outputs, r.filename, r.fileExtension);
});
self.setOption = function(...args) {};
/**
* Compress the files into a zip file and send the zip back
* to the OutputWaiter.
*
* @param {object} outputs
* @param {string} filename
* @param {string} fileExtension
*/
self.zipFiles = async function(outputs, filename, fileExtension) {
const zip = new Zlib.Zip();
const inputNums = Object.keys(outputs);
for (let i = 0; i < inputNums.length; i++) {
const iNum = inputNums[i];
let ext = fileExtension;
const cloned = new Dish(outputs[iNum].data.dish);
const output = new Uint8Array(await cloned.get(Dish.ARRAY_BUFFER));
if (fileExtension === undefined || fileExtension === "") {
// Detect automatically
const types = detectFileType(output);
if (!types.length) {
ext = ".dat";
} else {
ext = `.${types[0].extension.split(",", 1)[0]}`;
}
}
const name = Utils.strToByteArray(iNum + ext);
zip.addFile(output, {filename: name});
}
const zippedFile = zip.compress();
self.postMessage({
zippedFile: zippedFile.buffer,
filename: filename
}, [zippedFile.buffer]);
};