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

@ -50,7 +50,7 @@ You can use as many operations as you like in simple or complex ways. Some examp
- Drag and drop
- Operations can be dragged in and out of the recipe list, or reorganised.
- Files up to 500MB can be dragged over the input box to load them directly into the browser.
- Files up to 2GB can be dragged over the input box to load them directly into the browser.
- Auto Bake
- Whenever you modify the input or the recipe, CyberChef will automatically "bake" for you and produce the output immediately.
- This can be turned off and operated manually if it is affecting performance (if the input is very large, for instance).
@ -67,7 +67,7 @@ You can use as many operations as you like in simple or complex ways. Some examp
- Highlighting
- When you highlight text in the input or output, the offset and length values will be displayed and, if possible, the corresponding data will be highlighted in the output or input respectively (example: [highlight the word 'question' in the input to see where it appears in the output][11]).
- Save to file and load from file
- You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 500MB are supported (depending on your browser), however some operations may take a very long time to run over this much data.
- You can save the output to a file at any time or load a file by dragging and dropping it into the input field. Files up to around 2GB are supported (depending on your browser), however some operations may take a very long time to run over this much data.
- CyberChef is entirely client-side
- It should be noted that none of your recipe configuration or input (either text or files) is ever sent to the CyberChef web server - all processing is carried out within your browser, on your own computer.
- Due to this feature, CyberChef can be compiled into a single HTML file. You can download this file and drop it into a virtual machine, share it with other people, or use it independently on your local machine.

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]);
};

View file

@ -82,6 +82,7 @@ module.exports = {
browser
.useCss()
.setValue("#input-text", "Don't Panic.")
.pause(1000)
.click("#bake");
// Check output