mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-23 08:16:17 -04:00
Merge branch 'feature/bombe' into feature/typex
This commit is contained in:
commit
2be642e4c9
61 changed files with 4621 additions and 635 deletions
756
src/core/lib/Bombe.mjs
Normal file
756
src/core/lib/Bombe.mjs
Normal file
|
@ -0,0 +1,756 @@
|
|||
/**
|
||||
* Emulation of the Bombe machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @author The National Museum of Computing - Bombe Rebuild Project
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import {Rotor, Plugboard, a2i, i2a} from "./Enigma";
|
||||
|
||||
/**
|
||||
* Convenience/optimisation subclass of Rotor
|
||||
*
|
||||
* This allows creating multiple Rotors which share backing maps, to avoid repeatedly parsing the
|
||||
* rotor spec strings and duplicating the maps in memory.
|
||||
*/
|
||||
class CopyRotor extends Rotor {
|
||||
/**
|
||||
* Return a copy of this Rotor.
|
||||
* @returns {Object}
|
||||
*/
|
||||
copy() {
|
||||
const clone = {
|
||||
map: this.map,
|
||||
revMap: this.revMap,
|
||||
pos: this.pos,
|
||||
step: this.step,
|
||||
transform: this.transform,
|
||||
revTransform: this.revTransform,
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node in the menu graph
|
||||
*
|
||||
* A node represents a cipher/plaintext letter.
|
||||
*/
|
||||
class Node {
|
||||
/**
|
||||
* Node constructor.
|
||||
* @param {number} letter - The plain/ciphertext letter this node represents (as a number).
|
||||
*/
|
||||
constructor(letter) {
|
||||
this.letter = letter;
|
||||
this.edges = new Set();
|
||||
this.visited = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edge in the menu graph
|
||||
*
|
||||
* An edge represents an Enigma machine transformation between two letters.
|
||||
*/
|
||||
class Edge {
|
||||
/**
|
||||
* Edge constructor - an Enigma machine mapping between letters
|
||||
* @param {number} pos - The rotor position, relative to the beginning of the crib, at this edge
|
||||
* @param {number} node1 - Letter at one end (as a number)
|
||||
* @param {number} node2 - Letter at the other end
|
||||
*/
|
||||
constructor(pos, node1, node2) {
|
||||
this.pos = pos;
|
||||
this.node1 = node1;
|
||||
this.node2 = node2;
|
||||
node1.edges.add(this);
|
||||
node2.edges.add(this);
|
||||
this.visited = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the node at one end of this edge, return the other end.
|
||||
* @param node {number} - The node we have
|
||||
* @returns {number}
|
||||
*/
|
||||
getOther(node) {
|
||||
return this.node1 === node ? this.node2 : this.node1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers
|
||||
* in the machine share the majority of their state, which is hosted in this class.
|
||||
*/
|
||||
class SharedScrambler {
|
||||
/**
|
||||
* SharedScrambler constructor.
|
||||
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||
* @param {Object} reflector - The reflector in use.
|
||||
*/
|
||||
constructor(rotors, reflector) {
|
||||
this.lowerCache = new Array(26);
|
||||
this.higherCache = new Array(26);
|
||||
for (let i=0; i<26; i++) {
|
||||
this.higherCache[i] = new Array(26);
|
||||
}
|
||||
this.changeRotors(rotors, reflector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotors and reflector in this SharedScrambler.
|
||||
* This takes care of flushing caches as well.
|
||||
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||
* @param {Object} reflector - The reflector in use.
|
||||
*/
|
||||
changeRotors(rotors, reflector) {
|
||||
this.reflector = reflector;
|
||||
this.rotors = rotors;
|
||||
this.rotorsRev = [].concat(rotors).reverse();
|
||||
this.cacheGen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotors forward.
|
||||
* @param {number} n - How many rotors to step. This includes the rotors which are not part of
|
||||
* the shared state, so should be 2 or more.
|
||||
*/
|
||||
step(n) {
|
||||
for (let i=0; i<n-1; i++) {
|
||||
this.rotors[i].step();
|
||||
}
|
||||
this.cacheGen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimisation: We pregenerate all routes through the machine with the top rotor removed,
|
||||
* as these rarely change. This saves a lot of lookups. This function generates this route
|
||||
* table.
|
||||
* We also just-in-time cache the full routes through the scramblers, because after stepping
|
||||
* the fast rotor some scramblers will be in states occupied by other scrambles on previous
|
||||
* iterations.
|
||||
*/
|
||||
cacheGen() {
|
||||
for (let i=0; i<26; i++) {
|
||||
this.lowerCache[i] = undefined;
|
||||
for (let j=0; j<26; j++) {
|
||||
this.higherCache[i][j] = undefined;
|
||||
}
|
||||
}
|
||||
for (let i=0; i<26; i++) {
|
||||
if (this.lowerCache[i] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
let letter = i;
|
||||
for (const rotor of this.rotors) {
|
||||
letter = rotor.transform(letter);
|
||||
}
|
||||
letter = this.reflector.transform(letter);
|
||||
for (const rotor of this.rotorsRev) {
|
||||
letter = rotor.revTransform(letter);
|
||||
}
|
||||
// By symmetry
|
||||
this.lowerCache[i] = letter;
|
||||
this.lowerCache[letter] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a letter through this (partial) scrambler.
|
||||
* @param {number} i - The letter
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(i) {
|
||||
return this.lowerCache[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrambler.
|
||||
*
|
||||
* This is effectively just an Enigma machine, but it only operates on one character at a time and
|
||||
* the stepping mechanism is different.
|
||||
*/
|
||||
class Scrambler {
|
||||
/** Scrambler constructor.
|
||||
* @param {Object} base - The SharedScrambler whose state this scrambler uses
|
||||
* @param {Object} rotor - The non-shared fast rotor in this scrambler
|
||||
* @param {number} pos - Position offset from start of crib
|
||||
* @param {number} end1 - Letter in menu this scrambler is attached to
|
||||
* @param {number} end2 - Other letter in menu this scrambler is attached to
|
||||
*/
|
||||
constructor(base, rotor, pos, end1, end2) {
|
||||
this.baseScrambler = base;
|
||||
this.initialPos = pos;
|
||||
this.changeRotor(rotor);
|
||||
this.end1 = end1;
|
||||
this.end2 = end2;
|
||||
// For efficiency reasons, we pull the relevant shared cache from the baseScrambler into
|
||||
// this object - this saves us a few pointer dereferences
|
||||
this.cache = this.baseScrambler.higherCache[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotor in this scrambler.
|
||||
* The position is reset automatically.
|
||||
* @param {Object} rotor - New rotor
|
||||
*/
|
||||
changeRotor(rotor) {
|
||||
this.rotor = rotor;
|
||||
this.rotor.pos += this.initialPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotor forward.
|
||||
*
|
||||
* The base SharedScrambler needs to be instructed to step separately.
|
||||
*/
|
||||
step() {
|
||||
// The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons.
|
||||
// ...but for optimisation reasons I'm going to cheat and not do that, as this vastly
|
||||
// simplifies caching the state of the majority of the scramblers. The results are the
|
||||
// same, just in a slightly different order.
|
||||
this.rotor.step();
|
||||
this.cache = this.baseScrambler.higherCache[this.rotor.pos];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run a letter through the scrambler.
|
||||
* @param {number} i - The letter to transform (as a number)
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(i) {
|
||||
let letter = i;
|
||||
const cached = this.cache[i];
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
letter = this.rotor.transform(letter);
|
||||
letter = this.baseScrambler.transform(letter);
|
||||
letter = this.rotor.revTransform(letter);
|
||||
this.cache[i] = letter;
|
||||
this.cache[letter] = i;
|
||||
return letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one letter in the menu this scrambler maps to, return the other.
|
||||
* @param end {number} - The node we have
|
||||
* @returns {number}
|
||||
*/
|
||||
getOtherEnd(end) {
|
||||
return this.end1 === end ? this.end2 : this.end1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the position this scrambler is set to.
|
||||
* Note that because of Enigma's stepping, you need to set an actual Enigma to the previous
|
||||
* position in order to get it to make a certain set of electrical connections when a button
|
||||
* is pressed - this function *does* take this into account.
|
||||
* However, as with the rest of the Bombe, it does not take stepping into account - the middle
|
||||
* and slow rotors are treated as static.
|
||||
* @return {string}
|
||||
*/
|
||||
getPos() {
|
||||
let result = "";
|
||||
// Roll back the fast rotor by one step
|
||||
let pos = Utils.mod(this.rotor.pos - 1, 26);
|
||||
result += i2a(pos);
|
||||
for (let i=0; i<this.baseScrambler.rotors.length; i++) {
|
||||
pos = this.baseScrambler.rotors[i].pos;
|
||||
result += i2a(pos);
|
||||
}
|
||||
return result.split("").reverse().join("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bombe simulator class.
|
||||
*/
|
||||
export class BombeMachine {
|
||||
/**
|
||||
* Construct a Bombe.
|
||||
*
|
||||
* Note that there is no handling of offsets here: the crib specified must exactly match the
|
||||
* ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no
|
||||
* matching characters between crib and ciphertext) but cannot check further - if it's wrong
|
||||
* your results will be wrong!
|
||||
*
|
||||
* There is also no handling of rotor stepping - if the target Enigma stepped in the middle of
|
||||
* your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to
|
||||
* configure on a real Bombe, but we're not clear on whether it was ever actually done for
|
||||
* real (there would almost certainly have been better ways of attacking in most situations
|
||||
* than attempting to exhaust options for the stepping point, but in some circumstances, e.g.
|
||||
* via Banburismus, the stepping point might have been known).
|
||||
*
|
||||
* @param {string[]} rotors - list of rotor spec strings (without step points!)
|
||||
* @param {Object} reflector - Reflector object
|
||||
* @param {string} ciphertext - The ciphertext to attack
|
||||
* @param {string} crib - Known plaintext for this ciphertext
|
||||
* @param {boolean} check - Whether to use the checking machine
|
||||
* @param {function} update - Function to call to send status updates (optional)
|
||||
*/
|
||||
constructor(rotors, reflector, ciphertext, crib, check, update=undefined) {
|
||||
if (ciphertext.length < crib.length) {
|
||||
throw new OperationError("Crib overruns supplied ciphertext");
|
||||
}
|
||||
if (crib.length < 2) {
|
||||
// This is the absolute bare minimum to be sane, and even then it's likely too short to
|
||||
// be useful
|
||||
throw new OperationError("Crib is too short");
|
||||
}
|
||||
if (crib.length > 25) {
|
||||
// A crib longer than this will definitely cause the middle rotor to step somewhere
|
||||
// A shorter crib is preferable to reduce this chance, of course
|
||||
throw new OperationError("Crib is too long");
|
||||
}
|
||||
for (let i=0; i<crib.length; i++) {
|
||||
if (ciphertext[i] === crib[i]) {
|
||||
throw new OperationError(`Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`);
|
||||
}
|
||||
}
|
||||
this.ciphertext = ciphertext;
|
||||
this.crib = crib;
|
||||
this.initRotors(rotors);
|
||||
this.check = check;
|
||||
this.updateFn = update;
|
||||
|
||||
const [mostConnected, edges] = this.makeMenu();
|
||||
|
||||
// This is the bundle of wires corresponding to the 26 letters within each of the 26
|
||||
// possible nodes in the menu
|
||||
this.wires = new Array(26*26);
|
||||
|
||||
// These are the pseudo-Engima devices corresponding to each edge in the menu, and the
|
||||
// nodes in the menu they each connect to
|
||||
this.scramblers = new Array();
|
||||
for (let i=0; i<26; i++) {
|
||||
this.scramblers.push(new Array());
|
||||
}
|
||||
this.sharedScrambler = new SharedScrambler(this.baseRotors.slice(1), reflector);
|
||||
this.allScramblers = new Array();
|
||||
this.indicator = undefined;
|
||||
for (const edge of edges) {
|
||||
const cRotor = this.baseRotors[0].copy();
|
||||
const end1 = a2i(edge.node1.letter);
|
||||
const end2 = a2i(edge.node2.letter);
|
||||
const scrambler = new Scrambler(this.sharedScrambler, cRotor, edge.pos, end1, end2);
|
||||
if (edge.pos === 0) {
|
||||
this.indicator = scrambler;
|
||||
}
|
||||
this.scramblers[end1].push(scrambler);
|
||||
this.scramblers[end2].push(scrambler);
|
||||
this.allScramblers.push(scrambler);
|
||||
}
|
||||
// The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and
|
||||
// use one of the actual scramblers if there's one in the right position, but if not we'll
|
||||
// just create one.
|
||||
if (this.indicator === undefined) {
|
||||
this.indicator = new Scrambler(this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined);
|
||||
this.allScramblers.push(this.indicator);
|
||||
}
|
||||
|
||||
this.testRegister = a2i(mostConnected.letter);
|
||||
// This is an arbitrary letter other than the most connected letter
|
||||
for (const edge of mostConnected.edges) {
|
||||
this.testInput = [this.testRegister, a2i(edge.getOther(mostConnected).letter)];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Rotor objects from list of rotor wiring strings.
|
||||
* @param {string[]} rotors - List of rotor wiring strings
|
||||
*/
|
||||
initRotors(rotors) {
|
||||
// This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe
|
||||
this.baseRotors = [];
|
||||
for (const rstr of rotors) {
|
||||
const rotor = new CopyRotor(rstr, "", "A", "A");
|
||||
this.baseRotors.push(rotor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotors and reflector in all components of this Bombe.
|
||||
* @param {string[]} rotors - List of rotor wiring strings
|
||||
* @param {Object} reflector - Reflector object
|
||||
*/
|
||||
changeRotors(rotors, reflector) {
|
||||
// At the end of the run, the rotors are all back in the same position they started
|
||||
this.initRotors(rotors);
|
||||
this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector);
|
||||
for (const scrambler of this.allScramblers) {
|
||||
scrambler.changeRotor(this.baseRotors[0].copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have a way of sending status messages, do so.
|
||||
* @param {...*} msg - Message to send.
|
||||
*/
|
||||
update(...msg) {
|
||||
if (this.updateFn !== undefined) {
|
||||
this.updateFn(...msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive depth-first search on the menu graph.
|
||||
* This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each
|
||||
* of those graphs.
|
||||
* @param {Object} node - Node object to start the search from
|
||||
* @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected
|
||||
* node, order of most connected node, list of edges in this sub-graph
|
||||
*/
|
||||
dfs(node) {
|
||||
let loops = 0;
|
||||
let nNodes = 1;
|
||||
let mostConnected = node;
|
||||
let nConnections = mostConnected.edges.size;
|
||||
let edges = new Set();
|
||||
node.visited = true;
|
||||
for (const edge of node.edges) {
|
||||
if (edge.visited) {
|
||||
// Already been here from the other end.
|
||||
continue;
|
||||
}
|
||||
edge.visited = true;
|
||||
edges.add(edge);
|
||||
const other = edge.getOther(node);
|
||||
if (other.visited) {
|
||||
// We have a loop, record that and continue
|
||||
loops += 1;
|
||||
continue;
|
||||
}
|
||||
// This is a newly visited node
|
||||
const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other);
|
||||
loops += oLoops;
|
||||
nNodes += oNNodes;
|
||||
edges = new Set([...edges, ...oEdges]);
|
||||
if (oNConnections > nConnections) {
|
||||
mostConnected = oMostConnected;
|
||||
nConnections = oNConnections;
|
||||
}
|
||||
}
|
||||
return [loops, nNodes, mostConnected, nConnections, edges];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a menu from the ciphertext and crib.
|
||||
* A menu is just a graph where letters in either the ciphertext or crib (Enigma is symmetric,
|
||||
* so there's no difference mathematically) are nodes and states of the Enigma machine itself
|
||||
* are the edges.
|
||||
* Additionally, we want a single connected graph, and of the subgraphs available, we want the
|
||||
* one with the most loops (since these generate feedback cycles which efficiently close off
|
||||
* disallowed states).
|
||||
* Finally, we want to identify the most connected node in that graph (as it's the best choice
|
||||
* of measurement point).
|
||||
* @returns [Object, Object[]] - the most connected node, and the list of edges in the subgraph
|
||||
*/
|
||||
makeMenu() {
|
||||
// First, we make a graph of all of the mappings given by the crib
|
||||
// Make all nodes first
|
||||
const nodes = new Map();
|
||||
for (const c of this.ciphertext + this.crib) {
|
||||
if (!nodes.has(c)) {
|
||||
const node = new Node(c);
|
||||
nodes.set(c, node);
|
||||
}
|
||||
}
|
||||
// Then all edges
|
||||
for (let i=0; i<this.crib.length; i++) {
|
||||
const a = this.crib[i];
|
||||
const b = this.ciphertext[i];
|
||||
new Edge(i, nodes.get(a), nodes.get(b));
|
||||
}
|
||||
// list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges]
|
||||
const graphs = [];
|
||||
// Then, for each unconnected subgraph, we count the number of loops and nodes
|
||||
for (const start of nodes.keys()) {
|
||||
if (nodes.get(start).visited) {
|
||||
continue;
|
||||
}
|
||||
const subgraph = this.dfs(nodes.get(start));
|
||||
graphs.push(subgraph);
|
||||
}
|
||||
// Return the subgraph with the most loops (ties broken by node count)
|
||||
graphs.sort((a, b) => {
|
||||
let result = b[0] - a[0];
|
||||
if (result === 0) {
|
||||
result = b[1] - a[1];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
this.nLoops = graphs[0][0];
|
||||
return [graphs[0][2], graphs[0][4]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal
|
||||
* board and via the scramblers), energise them too, recursively.
|
||||
* @param {number} i - Bombe wire bundle
|
||||
* @param {number} j - Bombe stecker hypothesis wire within bundle
|
||||
*/
|
||||
energise(i, j) {
|
||||
const idx = 26*i + j;
|
||||
if (this.wires[idx]) {
|
||||
return;
|
||||
}
|
||||
this.wires[idx] = true;
|
||||
// Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle
|
||||
// both.
|
||||
const idxPair = 26*j + i;
|
||||
this.wires[idxPair] = true;
|
||||
if (i === this.testRegister || j === this.testRegister) {
|
||||
this.energiseCount++;
|
||||
if (this.energiseCount === 26) {
|
||||
// no point continuing, bail out
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (let k=0; k<this.scramblers[i].length; k++) {
|
||||
const scrambler = this.scramblers[i][k];
|
||||
const out = scrambler.transform(j);
|
||||
const other = scrambler.getOtherEnd(i);
|
||||
// Lift the pre-check before the call, to save some function call overhead
|
||||
const otherIdx = 26*other + out;
|
||||
if (!this.wires[otherIdx]) {
|
||||
this.energise(other, out);
|
||||
if (this.energiseCount === 26) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
for (let k=0; k<this.scramblers[j].length; k++) {
|
||||
const scrambler = this.scramblers[j][k];
|
||||
const out = scrambler.transform(i);
|
||||
const other = scrambler.getOtherEnd(j);
|
||||
const otherIdx = 26*other + out;
|
||||
if (!this.wires[otherIdx]) {
|
||||
this.energise(other, out);
|
||||
if (this.energiseCount === 26) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trial decryption at the current setting.
|
||||
* Used after we get a stop.
|
||||
* This applies the detected stecker pair if we have one. It does not handle the other
|
||||
* steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to
|
||||
* be wrong after that anyway).
|
||||
* @param {string} stecker - Known stecker spec string.
|
||||
* @returns {string}
|
||||
*/
|
||||
tryDecrypt(stecker) {
|
||||
const fastRotor = this.indicator.rotor;
|
||||
const initialPos = fastRotor.pos;
|
||||
const res = [];
|
||||
const plugboard = new Plugboard(stecker);
|
||||
// The indicator scrambler starts in the right place for the beginning of the ciphertext.
|
||||
for (let i=0; i<Math.min(26, this.ciphertext.length); i++) {
|
||||
const t = this.indicator.transform(plugboard.transform(a2i(this.ciphertext[i])));
|
||||
res.push(i2a(plugboard.transform(t)));
|
||||
this.indicator.step(1);
|
||||
}
|
||||
fastRotor.pos = initialPos;
|
||||
return res.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a steckered pair, in sorted order to allow uniquing.
|
||||
* @param {number} a - A letter
|
||||
* @param {number} b - Its stecker pair
|
||||
* @returns {string}
|
||||
*/
|
||||
formatPair(a, b) {
|
||||
if (a < b) {
|
||||
return `${i2a(a)}${i2a(b)}`;
|
||||
}
|
||||
return `${i2a(b)}${i2a(a)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The checking machine was used to manually verify Bombe stops. Using a device which was
|
||||
* effectively a non-stepping Enigma, the user would walk through each of the links in the
|
||||
* menu at the rotor positions determined by the Bombe. By starting with the stecker pair the
|
||||
* Bombe gives us, we find the stecker pair of each connected letter in the graph, and so on.
|
||||
* If a contradiction is reached, the stop is invalid. If not, we have most (but not
|
||||
* necessarily all) of the plugboard connections.
|
||||
* You will notice that this procedure is exactly the same as what the Bombe itself does, only
|
||||
* we start with an assumed good hypothesis and read out the stecker pair for every letter.
|
||||
* On the real hardware that wasn't practical, but fortunately we're not the real hardware, so
|
||||
* we don't need to implement the manual checking machine procedure.
|
||||
* @param {number} pair - The stecker pair of the test register.
|
||||
* @returns {string} - The empty string for invalid stops, or a plugboard configuration string
|
||||
* containing all known pairs.
|
||||
*/
|
||||
checkingMachine(pair) {
|
||||
if (pair !== this.testInput[1]) {
|
||||
// We have a new hypothesis for this stop - apply the new one.
|
||||
// De-energise the board
|
||||
for (let i=0; i<this.wires.length; i++) {
|
||||
this.wires[i] = false;
|
||||
}
|
||||
this.energiseCount = 0;
|
||||
// Re-energise with the corrected hypothesis
|
||||
this.energise(this.testRegister, pair);
|
||||
}
|
||||
|
||||
const results = new Set();
|
||||
results.add(this.formatPair(this.testRegister, pair));
|
||||
for (let i=0; i<26; i++) {
|
||||
let count = 0;
|
||||
let other;
|
||||
for (let j=0; j<26; j++) {
|
||||
if (this.wires[i*26 + j]) {
|
||||
count++;
|
||||
other = j;
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
// This is an invalid stop.
|
||||
return "";
|
||||
} else if (count === 0) {
|
||||
// No information about steckering from this wire
|
||||
continue;
|
||||
}
|
||||
results.add(this.formatPair(i, other));
|
||||
}
|
||||
return [...results].join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the Bombe has stopped. If so, process the stop.
|
||||
* @returns {(undefined|string[3])} - Undefined for no stop, or [rotor settings, plugboard settings, decryption preview]
|
||||
*/
|
||||
checkStop() {
|
||||
// Count the energised outputs
|
||||
const count = this.energiseCount;
|
||||
if (count === 26) {
|
||||
return undefined;
|
||||
}
|
||||
// If it's not all of them, we have a stop
|
||||
let steckerPair;
|
||||
// The Bombe tells us one stecker pair as well. The input wire and test register we
|
||||
// started with are hypothesised to be a stecker pair.
|
||||
if (count === 25) {
|
||||
// Our steckering hypothesis is wrong. Correct value is the un-energised wire.
|
||||
for (let j=0; j<26; j++) {
|
||||
if (!this.wires[26*this.testRegister + j]) {
|
||||
steckerPair = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (count === 1) {
|
||||
// This means our hypothesis for the steckering is correct.
|
||||
steckerPair = this.testInput[1];
|
||||
} else {
|
||||
// This was known as a "boxing stop" - we have a stop but not a single hypothesis.
|
||||
// If this happens a lot it implies the menu isn't good enough.
|
||||
// If we have the checking machine enabled, we're going to just check each wire in
|
||||
// turn. If we get 0 or 1 hit, great.
|
||||
// If we get multiple hits, or the checking machine is off, the user will just have to
|
||||
// deal with it.
|
||||
if (!this.check) {
|
||||
// We can't draw any conclusions about the steckering (one could maybe suggest
|
||||
// options in some cases, but too hard to present clearly).
|
||||
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||
}
|
||||
let stecker = undefined;
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const newStecker = this.checkingMachine(i);
|
||||
if (newStecker !== "") {
|
||||
if (stecker !== undefined) {
|
||||
// Multiple hypotheses can't be ruled out.
|
||||
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||
}
|
||||
stecker = newStecker;
|
||||
}
|
||||
}
|
||||
if (stecker === undefined) {
|
||||
// Checking machine ruled all possibilities out.
|
||||
return undefined;
|
||||
}
|
||||
// If we got here, there was just one possibility allowed by the checking machine. Success.
|
||||
return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)];
|
||||
}
|
||||
let stecker;
|
||||
if (this.check) {
|
||||
stecker = this.checkingMachine(steckerPair);
|
||||
if (stecker === "") {
|
||||
// Invalid stop - don't count it, don't return it
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
stecker = `${i2a(this.testRegister)}${i2a(steckerPair)}`;
|
||||
}
|
||||
const testDecrypt = this.tryDecrypt(stecker);
|
||||
return [this.indicator.getPos(), stecker, testDecrypt];
|
||||
}
|
||||
|
||||
/**
|
||||
* Having set up the Bombe, do the actual attack run. This tries every possible rotor setting
|
||||
* and attempts to logically invalidate them. If it can't, it's added to the list of candidate
|
||||
* solutions.
|
||||
* @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview
|
||||
*/
|
||||
run() {
|
||||
let stops = 0;
|
||||
const result = [];
|
||||
// For each possible rotor setting
|
||||
const nChecks = Math.pow(26, this.baseRotors.length);
|
||||
for (let i=1; i<=nChecks; i++) {
|
||||
// Benchmarking suggests this is faster than using .fill()
|
||||
for (let i=0; i<this.wires.length; i++) {
|
||||
this.wires[i] = false;
|
||||
}
|
||||
this.energiseCount = 0;
|
||||
// Energise the test input, follow the current through each scrambler
|
||||
// (and the diagonal board)
|
||||
this.energise(...this.testInput);
|
||||
|
||||
const stop = this.checkStop();
|
||||
if (stop !== undefined) {
|
||||
stops++;
|
||||
result.push(stop);
|
||||
}
|
||||
// Step all the scramblers
|
||||
// This loop counts how many rotors have reached their starting position (meaning the
|
||||
// next one needs to step as well)
|
||||
let n = 1;
|
||||
for (let j=1; j<this.baseRotors.length; j++) {
|
||||
if ((i % Math.pow(26, j)) === 0) {
|
||||
n++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n > 1) {
|
||||
this.sharedScrambler.step(n);
|
||||
}
|
||||
for (const scrambler of this.allScramblers) {
|
||||
scrambler.step();
|
||||
}
|
||||
// Send status messages at what seems to be a reasonably sensible frequency
|
||||
// (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary)
|
||||
if (n > 3) {
|
||||
this.update(this.nLoops, stops, i/nChecks);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
655
src/core/lib/ConvertCoordinates.mjs
Normal file
655
src/core/lib/ConvertCoordinates.mjs
Normal file
|
@ -0,0 +1,655 @@
|
|||
/**
|
||||
* Co-ordinate conversion resources.
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import geohash from "ngeohash";
|
||||
import geodesy from "geodesy";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
* Co-ordinate formats
|
||||
*/
|
||||
export const FORMATS = [
|
||||
"Degrees Minutes Seconds",
|
||||
"Degrees Decimal Minutes",
|
||||
"Decimal Degrees",
|
||||
"Geohash",
|
||||
"Military Grid Reference System",
|
||||
"Ordnance Survey National Grid",
|
||||
"Universal Transverse Mercator"
|
||||
];
|
||||
|
||||
/**
|
||||
* Formats that should be passed to the conversion module as-is
|
||||
*/
|
||||
const NO_CHANGE = [
|
||||
"Geohash",
|
||||
"Military Grid Reference System",
|
||||
"Ordnance Survey National Grid",
|
||||
"Universal Transverse Mercator",
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a given latitude and longitude into a different format.
|
||||
*
|
||||
* @param {string} input - Input string to be converted
|
||||
* @param {string} inFormat - Format of the input coordinates
|
||||
* @param {string} inDelim - The delimiter splitting the lat/long of the input
|
||||
* @param {string} outFormat - Format to convert to
|
||||
* @param {string} outDelim - The delimiter to separate the output with
|
||||
* @param {string} includeDir - Whether or not to include the compass direction in the output
|
||||
* @param {number} precision - Precision of the result
|
||||
* @returns {string} A formatted string of the converted co-ordinates
|
||||
*/
|
||||
export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) {
|
||||
let isPair = false,
|
||||
split,
|
||||
latlon,
|
||||
convLat,
|
||||
convLon,
|
||||
conv,
|
||||
hash,
|
||||
utm,
|
||||
mgrs,
|
||||
osng,
|
||||
splitLat,
|
||||
splitLong,
|
||||
lat,
|
||||
lon;
|
||||
|
||||
// Can't have a precision less than 0!
|
||||
if (precision < 0) {
|
||||
precision = 0;
|
||||
}
|
||||
|
||||
if (inDelim === "Auto") {
|
||||
// Try to detect a delimiter in the input.
|
||||
inDelim = findDelim(input);
|
||||
if (inDelim === null) {
|
||||
throw new OperationError("Unable to detect the input delimiter automatically.");
|
||||
}
|
||||
} else if (!inDelim.includes("Direction")) {
|
||||
// Convert the delimiter argument value to the actual character
|
||||
inDelim = realDelim(inDelim);
|
||||
}
|
||||
|
||||
if (inFormat === "Auto") {
|
||||
// Try to detect the format of the input data
|
||||
inFormat = findFormat(input, inDelim);
|
||||
if (inFormat === null) {
|
||||
throw new OperationError("Unable to detect the input format automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the output delimiter argument to the real character
|
||||
outDelim = realDelim(outDelim);
|
||||
|
||||
if (!NO_CHANGE.includes(inFormat)) {
|
||||
if (inDelim.includes("Direction")) {
|
||||
// Split on directions
|
||||
split = input.split(/[NnEeSsWw]/g);
|
||||
if (split[0] === "") {
|
||||
// Remove first element if direction preceding
|
||||
split = split.slice(1);
|
||||
}
|
||||
} else {
|
||||
split = input.split(inDelim);
|
||||
}
|
||||
// Replace any co-ordinate symbols with spaces so we can split on them later
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
split[i] = split[i].replace(/[°˝´'"]/g, " ");
|
||||
}
|
||||
if (split.length > 1) {
|
||||
isPair = true;
|
||||
}
|
||||
} else {
|
||||
// Remove any delimiters from the input
|
||||
input = input.replace(inDelim, "");
|
||||
isPair = true;
|
||||
}
|
||||
|
||||
// Conversions from the input format into a geodesy latlon object
|
||||
switch (inFormat) {
|
||||
case "Geohash":
|
||||
hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, ""));
|
||||
latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude);
|
||||
break;
|
||||
case "Military Grid Reference System":
|
||||
utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
|
||||
latlon = utm.toLatLonE();
|
||||
break;
|
||||
case "Ordnance Survey National Grid":
|
||||
osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
|
||||
latlon = geodesy.OsGridRef.osGridToLatLon(osng);
|
||||
break;
|
||||
case "Universal Transverse Mercator":
|
||||
// Geodesy needs a space between the first 2 digits and the next letter
|
||||
if (/^[\d]{2}[A-Za-z]/.test(input)) {
|
||||
input = input.slice(0, 2) + " " + input.slice(2);
|
||||
}
|
||||
utm = geodesy.Utm.parse(input);
|
||||
latlon = utm.toLatLonE();
|
||||
break;
|
||||
case "Degrees Minutes Seconds":
|
||||
if (isPair) {
|
||||
// Split up the lat/long into degrees / minutes / seconds values
|
||||
splitLat = splitInput(split[0]);
|
||||
splitLong = splitInput(split[1]);
|
||||
|
||||
if (splitLat.length >= 3 && splitLong.length >= 3) {
|
||||
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10);
|
||||
lon = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10);
|
||||
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
|
||||
} else {
|
||||
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
|
||||
}
|
||||
} else {
|
||||
// Not a pair, so only try to convert one set of co-ordinates
|
||||
splitLat = splitInput(split[0]);
|
||||
if (splitLat.length >= 3) {
|
||||
lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]);
|
||||
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
|
||||
} else {
|
||||
throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Degrees Decimal Minutes":
|
||||
if (isPair) {
|
||||
splitLat = splitInput(split[0]);
|
||||
splitLong = splitInput(split[1]);
|
||||
if (splitLat.length !== 2 || splitLong.length !== 2) {
|
||||
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
|
||||
}
|
||||
// Convert to decimal degrees, and then convert to a geodesy object
|
||||
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
|
||||
lon = convDDMToDD(splitLong[0], splitLong[1], 10);
|
||||
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
|
||||
} else {
|
||||
// Not a pair, so only try to convert one set of co-ordinates
|
||||
splitLat = splitInput(input);
|
||||
if (splitLat.length !== 2) {
|
||||
throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
|
||||
}
|
||||
lat = convDDMToDD(splitLat[0], splitLat[1], 10);
|
||||
latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
|
||||
}
|
||||
break;
|
||||
case "Decimal Degrees":
|
||||
if (isPair) {
|
||||
splitLat = splitInput(split[0]);
|
||||
splitLong = splitInput(split[1]);
|
||||
if (splitLat.length !== 1 || splitLong.length !== 1) {
|
||||
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
|
||||
}
|
||||
latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLong[0]);
|
||||
} else {
|
||||
// Not a pair, so only try to convert one set of co-ordinates
|
||||
splitLat = splitInput(split[0]);
|
||||
if (splitLat.length !== 1) {
|
||||
throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
|
||||
}
|
||||
latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new OperationError(`Unknown input format '${inFormat}'`);
|
||||
}
|
||||
|
||||
// Everything is now a geodesy latlon object
|
||||
// These store the latitude and longitude as decimal
|
||||
if (inFormat.includes("Degrees")) {
|
||||
// If the input string contains directions, we need to check if they're S or W.
|
||||
// If either of the directions are, we should make the decimal value negative
|
||||
const dirs = input.toUpperCase().match(/[NESW]/g);
|
||||
if (dirs && dirs.length >= 1) {
|
||||
// Make positive lat/lon values with S/W directions into negative values
|
||||
if (dirs[0] === "S" || dirs[0] === "W" && latlon.lat > 0) {
|
||||
latlon.lat = -latlon.lat;
|
||||
}
|
||||
if (dirs.length >= 2) {
|
||||
if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) {
|
||||
latlon.lon = -latlon.lon;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the compass directions of the lat and long
|
||||
const [latDir, longDir] = findDirs(latlon.lat + "," + latlon.lon, ",");
|
||||
|
||||
// Output conversions for each output format
|
||||
switch (outFormat) {
|
||||
case "Decimal Degrees":
|
||||
// We could use the built in latlon.toString(),
|
||||
// but this makes adjusting the output harder
|
||||
lat = convDDToDD(latlon.lat, precision);
|
||||
lon = convDDToDD(latlon.lon, precision);
|
||||
convLat = lat.string;
|
||||
convLon = lon.string;
|
||||
break;
|
||||
case "Degrees Decimal Minutes":
|
||||
lat = convDDToDDM(latlon.lat, precision);
|
||||
lon = convDDToDDM(latlon.lon, precision);
|
||||
convLat = lat.string;
|
||||
convLon = lon.string;
|
||||
break;
|
||||
case "Degrees Minutes Seconds":
|
||||
lat = convDDToDMS(latlon.lat, precision);
|
||||
lon = convDDToDMS(latlon.lon, precision);
|
||||
convLat = lat.string;
|
||||
convLon = lon.string;
|
||||
break;
|
||||
case "Geohash":
|
||||
convLat = geohash.encode(latlon.lat, latlon.lon, precision);
|
||||
break;
|
||||
case "Military Grid Reference System":
|
||||
utm = latlon.toUtm();
|
||||
mgrs = utm.toMgrs();
|
||||
// MGRS wants a precision that's an even number between 2 and 10
|
||||
if (precision % 2 !== 0) {
|
||||
precision = precision + 1;
|
||||
}
|
||||
if (precision > 10) {
|
||||
precision = 10;
|
||||
}
|
||||
convLat = mgrs.toString(precision);
|
||||
break;
|
||||
case "Ordnance Survey National Grid":
|
||||
osng = geodesy.OsGridRef.latLonToOsGrid(latlon);
|
||||
if (osng.toString() === "") {
|
||||
throw new OperationError("Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?");
|
||||
}
|
||||
// OSNG wants a precision that's an even number between 2 and 10
|
||||
if (precision % 2 !== 0) {
|
||||
precision = precision + 1;
|
||||
}
|
||||
if (precision > 10) {
|
||||
precision = 10;
|
||||
}
|
||||
convLat = osng.toString(precision);
|
||||
break;
|
||||
case "Universal Transverse Mercator":
|
||||
utm = latlon.toUtm();
|
||||
convLat = utm.toString(precision);
|
||||
break;
|
||||
}
|
||||
|
||||
if (convLat === undefined) {
|
||||
throw new OperationError("Error converting co-ordinates.");
|
||||
}
|
||||
|
||||
if (outFormat.includes("Degrees")) {
|
||||
// Format DD/DDM/DMS for output
|
||||
// If we're outputting a compass direction, remove the negative sign
|
||||
if (latDir === "S" && includeDir !== "None") {
|
||||
convLat = convLat.replace("-", "");
|
||||
}
|
||||
if (longDir === "W" && includeDir !== "None") {
|
||||
convLon = convLon.replace("-", "");
|
||||
}
|
||||
|
||||
let outConv = "";
|
||||
if (includeDir === "Before") {
|
||||
outConv += latDir + " ";
|
||||
}
|
||||
|
||||
outConv += convLat;
|
||||
if (includeDir === "After") {
|
||||
outConv += " " + latDir;
|
||||
}
|
||||
outConv += outDelim;
|
||||
if (isPair) {
|
||||
if (includeDir === "Before") {
|
||||
outConv += longDir + " ";
|
||||
}
|
||||
outConv += convLon;
|
||||
if (includeDir === "After") {
|
||||
outConv += " " + longDir;
|
||||
}
|
||||
outConv += outDelim;
|
||||
}
|
||||
conv = outConv;
|
||||
} else {
|
||||
conv = convLat + outDelim;
|
||||
}
|
||||
|
||||
return conv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split up the input using a space or degrees signs, and sanitise the result
|
||||
*
|
||||
* @param {string} input - The input data to be split
|
||||
* @returns {number[]} An array of the different items in the string, stored as floats
|
||||
*/
|
||||
function splitInput (input){
|
||||
const split = [];
|
||||
|
||||
input.split(/\s+/).forEach(item => {
|
||||
// Remove any character that isn't a digit, decimal point or negative sign
|
||||
item = item.replace(/[^0-9.-]/g, "");
|
||||
if (item.length > 0){
|
||||
// Turn the item into a float
|
||||
split.push(parseFloat(item));
|
||||
}
|
||||
});
|
||||
return split;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Degrees Minutes Seconds to Decimal Degrees
|
||||
*
|
||||
* @param {number} degrees - The degrees of the input co-ordinates
|
||||
* @param {number} minutes - The minutes of the input co-ordinates
|
||||
* @param {number} seconds - The seconds of the input co-ordinates
|
||||
* @param {number} precision - The precision the result should be rounded to
|
||||
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||
*/
|
||||
function convDMSToDD (degrees, minutes, seconds, precision){
|
||||
const absDegrees = Math.abs(degrees);
|
||||
let conv = absDegrees + (minutes / 60) + (seconds / 3600);
|
||||
let outString = round(conv, precision) + "°";
|
||||
if (isNegativeZero(degrees) || degrees < 0) {
|
||||
conv = -conv;
|
||||
outString = "-" + outString;
|
||||
}
|
||||
return {
|
||||
"degrees": conv,
|
||||
"string": outString
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Decimal Degrees Minutes to Decimal Degrees
|
||||
*
|
||||
* @param {number} degrees - The input degrees to be converted
|
||||
* @param {number} minutes - The input minutes to be converted
|
||||
* @param {number} precision - The precision which the result should be rounded to
|
||||
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||
*/
|
||||
function convDDMToDD (degrees, minutes, precision) {
|
||||
const absDegrees = Math.abs(degrees);
|
||||
let conv = absDegrees + minutes / 60;
|
||||
let outString = round(conv, precision) + "°";
|
||||
if (isNegativeZero(degrees) || degrees < 0) {
|
||||
conv = -conv;
|
||||
outString = "-" + outString;
|
||||
}
|
||||
return {
|
||||
"degrees": conv,
|
||||
"string": outString
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Decimal Degrees to Decimal Degrees
|
||||
*
|
||||
* Doesn't affect the input, just puts it into an object
|
||||
* @param {number} degrees - The input degrees to be converted
|
||||
* @param {number} precision - The precision which the result should be rounded to
|
||||
* @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
|
||||
*/
|
||||
function convDDToDD (degrees, precision) {
|
||||
return {
|
||||
"degrees": degrees,
|
||||
"string": round(degrees, precision) + "°"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Decimal Degrees to Degrees Minutes Seconds
|
||||
*
|
||||
* @param {number} decDegrees - The input data to be converted
|
||||
* @param {number} precision - The precision which the result should be rounded to
|
||||
* @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string)
|
||||
*/
|
||||
function convDDToDMS (decDegrees, precision) {
|
||||
const absDegrees = Math.abs(decDegrees);
|
||||
let degrees = Math.floor(absDegrees);
|
||||
const minutes = Math.floor(60 * (absDegrees - degrees)),
|
||||
seconds = round(3600 * (absDegrees - degrees) - 60 * minutes, precision);
|
||||
let outString = degrees + "° " + minutes + "' " + seconds + "\"";
|
||||
if (isNegativeZero(decDegrees) || decDegrees < 0) {
|
||||
degrees = -degrees;
|
||||
outString = "-" + outString;
|
||||
}
|
||||
return {
|
||||
"degrees": degrees,
|
||||
"minutes": minutes,
|
||||
"seconds": seconds,
|
||||
"string": outString
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Decimal Degrees to Degrees Decimal Minutes
|
||||
*
|
||||
* @param {number} decDegrees - The input degrees to be converted
|
||||
* @param {number} precision - The precision the input data should be rounded to
|
||||
* @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string)
|
||||
*/
|
||||
function convDDToDDM (decDegrees, precision) {
|
||||
const absDegrees = Math.abs(decDegrees);
|
||||
let degrees = Math.floor(absDegrees);
|
||||
const minutes = absDegrees - degrees,
|
||||
decMinutes = round(minutes * 60, precision);
|
||||
let outString = degrees + "° " + decMinutes + "'";
|
||||
if (decDegrees < 0 || isNegativeZero(decDegrees)) {
|
||||
degrees = -degrees;
|
||||
outString = "-" + outString;
|
||||
}
|
||||
|
||||
return {
|
||||
"degrees": degrees,
|
||||
"minutes": decMinutes,
|
||||
"string": outString,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the compass directions in an input string
|
||||
*
|
||||
* @param {string} input - The input co-ordinates containing the direction
|
||||
* @param {string} delim - The delimiter separating latitide and longitude
|
||||
* @returns {string[]} String array containing the latitude and longitude directions
|
||||
*/
|
||||
export function findDirs(input, delim) {
|
||||
const upperInput = input.toUpperCase();
|
||||
const dirExp = new RegExp(/[NESW]/g);
|
||||
|
||||
const dirs = upperInput.match(dirExp);
|
||||
|
||||
if (dirs) {
|
||||
// If there's actually compass directions
|
||||
// in the input, use these to work out the direction
|
||||
if (dirs.length <= 2 && dirs.length >= 1) {
|
||||
return dirs.length === 2 ? [dirs[0], dirs[1]] : [dirs[0], ""];
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing was returned, so guess the directions
|
||||
let lat = upperInput,
|
||||
long,
|
||||
latDir = "",
|
||||
longDir = "";
|
||||
if (!delim.includes("Direction")) {
|
||||
if (upperInput.includes(delim)) {
|
||||
const split = upperInput.split(delim);
|
||||
if (split.length >= 1) {
|
||||
if (split[0] !== "") {
|
||||
lat = split[0];
|
||||
}
|
||||
if (split.length >= 2 && split[1] !== "") {
|
||||
long = split[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const split = upperInput.split(dirExp);
|
||||
if (split.length > 1) {
|
||||
lat = split[0] === "" ? split[1] : split[0];
|
||||
if (split.length > 2 && split[2] !== "") {
|
||||
long = split[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lat) {
|
||||
lat = parseFloat(lat);
|
||||
latDir = lat < 0 ? "S" : "N";
|
||||
}
|
||||
|
||||
if (long) {
|
||||
long = parseFloat(long);
|
||||
longDir = long < 0 ? "W" : "E";
|
||||
}
|
||||
|
||||
return [latDir, longDir];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the co-ordinate format of the input data
|
||||
*
|
||||
* @param {string} input - The input data whose format we need to detect
|
||||
* @param {string} delim - The delimiter separating the data in input
|
||||
* @returns {string} The input format
|
||||
*/
|
||||
export function findFormat (input, delim) {
|
||||
let testData;
|
||||
const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/),
|
||||
osngPattern = new RegExp(/^[A-HJ-Z]{2}\s+[0-9\s]+$/),
|
||||
geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/),
|
||||
utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9.]+\s?[0-9.]+$/),
|
||||
degPattern = new RegExp(/[°'"]/g);
|
||||
|
||||
input = input.trim();
|
||||
|
||||
if (delim !== null && delim.includes("Direction")) {
|
||||
const split = input.split(/[NnEeSsWw]/);
|
||||
if (split.length > 1) {
|
||||
testData = split[0] === "" ? split[1] : split[0];
|
||||
}
|
||||
} else if (delim !== null && delim !== "") {
|
||||
if (input.includes(delim)) {
|
||||
const split = input.split(delim);
|
||||
if (split.length > 1) {
|
||||
testData = split[0] === "" ? split[1] : split[0];
|
||||
}
|
||||
} else {
|
||||
testData = input;
|
||||
}
|
||||
}
|
||||
|
||||
// Test non-degrees formats
|
||||
if (!degPattern.test(input)) {
|
||||
const filteredInput = input.toUpperCase().replace(delim, "");
|
||||
|
||||
if (utmPattern.test(filteredInput)) {
|
||||
return "Universal Transverse Mercator";
|
||||
}
|
||||
if (mgrsPattern.test(filteredInput)) {
|
||||
return "Military Grid Reference System";
|
||||
}
|
||||
if (osngPattern.test(filteredInput)) {
|
||||
return "Ordnance Survey National Grid";
|
||||
}
|
||||
if (geohashPattern.test(filteredInput)) {
|
||||
return "Geohash";
|
||||
}
|
||||
}
|
||||
|
||||
// Test DMS/DDM/DD formats
|
||||
if (testData !== undefined) {
|
||||
const split = splitInput(testData);
|
||||
switch (split.length){
|
||||
case 3:
|
||||
return "Degrees Minutes Seconds";
|
||||
case 2:
|
||||
return "Degrees Decimal Minutes";
|
||||
case 1:
|
||||
return "Decimal Degrees";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically find the delimeter type from the given input
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string} Delimiter type
|
||||
*/
|
||||
export function findDelim (input) {
|
||||
input = input.trim();
|
||||
const delims = [",", ";", ":"];
|
||||
const testDir = input.match(/[NnEeSsWw]/g);
|
||||
if (testDir !== null && testDir.length > 0 && testDir.length < 3) {
|
||||
// Possibly contains a direction
|
||||
const splitInput = input.split(/[NnEeSsWw]/);
|
||||
if (splitInput.length <= 3 && splitInput.length > 0) {
|
||||
// If there's 3 splits (one should be empty), then assume we have directions
|
||||
if (splitInput[0] === "") {
|
||||
return "Direction Preceding";
|
||||
} else if (splitInput[splitInput.length - 1] === "") {
|
||||
return "Direction Following";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the standard delimiters, and try to find them in the input
|
||||
for (let i = 0; i < delims.length; i++) {
|
||||
const delim = delims[i];
|
||||
if (input.includes(delim)) {
|
||||
const splitInput = input.split(delim);
|
||||
if (splitInput.length <= 3 && splitInput.length > 0) {
|
||||
// Don't want to try and convert more than 2 co-ordinates
|
||||
return delim;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the real string for a delimiter name.
|
||||
*
|
||||
* @param {string} delim The delimiter to be matched
|
||||
* @returns {string}
|
||||
*/
|
||||
export function realDelim (delim) {
|
||||
return {
|
||||
"Auto": "Auto",
|
||||
"Space": " ",
|
||||
"\\n": "\n",
|
||||
"Comma": ",",
|
||||
"Semi-colon": ";",
|
||||
"Colon": ":"
|
||||
}[delim];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a zero is negative
|
||||
*
|
||||
* @param {number} zero
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isNegativeZero(zero) {
|
||||
return zero === 0 && (1/zero < 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a number to a specified number of decimal places
|
||||
*
|
||||
* @param {number} input - The number to be rounded
|
||||
* @param {precision} precision - The number of decimal places the number should be rounded to
|
||||
* @returns {number}
|
||||
*/
|
||||
function round(input, precision) {
|
||||
precision = Math.pow(10, precision);
|
||||
return Math.round(input * precision) / precision;
|
||||
}
|
|
@ -22,14 +22,13 @@ export const ROTORS = [
|
|||
{name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW<AN"},
|
||||
{name: "VII", value: "NZJHGRCXMYSWBOUFAIVLPEKQDT<AN"},
|
||||
{name: "VIII", value: "FKQHTLXOCBJSPDZRAMEWNIUYGV<AN"},
|
||||
];
|
||||
|
||||
export const ROTORS_FOURTH = [
|
||||
{name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"},
|
||||
{name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"},
|
||||
];
|
||||
|
||||
export const ROTORS_OPTIONAL = [].concat(ROTORS).concat([
|
||||
{name: "None", value: ""},
|
||||
]);
|
||||
|
||||
/**
|
||||
* Provided default Enigma reflector set.
|
||||
* These are specified as 13 space-separated transposed pairs covering every letter.
|
||||
|
@ -103,15 +102,17 @@ export class Rotor {
|
|||
if (!/^[A-Z]$/.test(initialPosition)) {
|
||||
throw new OperationError("Rotor initial position must be exactly one uppercase letter");
|
||||
}
|
||||
this.map = {};
|
||||
this.revMap = {};
|
||||
this.map = new Array(26);
|
||||
this.revMap = new Array(26);
|
||||
const uniq = {};
|
||||
for (let i=0; i<LETTERS.length; i++) {
|
||||
const a = a2i(LETTERS[i]);
|
||||
const b = a2i(wiring[i]);
|
||||
this.map[a] = b;
|
||||
this.revMap[b] = a;
|
||||
uniq[b] = true;
|
||||
}
|
||||
if (Object.keys(this.revMap).length !== LETTERS.length) {
|
||||
if (Object.keys(uniq).length !== LETTERS.length) {
|
||||
throw new OperationError("Rotor wiring must have each letter exactly once");
|
||||
}
|
||||
const rs = a2i(ringSetting);
|
||||
|
@ -169,6 +170,7 @@ class PairMapBase {
|
|||
constructor(pairs, name="PairMapBase") {
|
||||
// I've chosen to make whitespace significant here to make a) code and
|
||||
// b) inputs easier to read
|
||||
this.pairs = pairs;
|
||||
this.map = {};
|
||||
if (pairs === "") {
|
||||
return;
|
||||
|
@ -179,7 +181,8 @@ class PairMapBase {
|
|||
}
|
||||
const a = a2i(pair[0]), b = a2i(pair[1]);
|
||||
if (a === b) {
|
||||
throw new OperationError(`${name}: cannot connect ${pair[0]} to itself`);
|
||||
// self-stecker
|
||||
return;
|
||||
}
|
||||
if (this.map.hasOwnProperty(a)) {
|
||||
throw new OperationError(`${name} connects ${pair[0]} more than once`);
|
||||
|
@ -219,6 +222,8 @@ class PairMapBase {
|
|||
|
||||
/**
|
||||
* Reflector. PairMapBase but requires that all characters are accounted for.
|
||||
*
|
||||
* Includes a couple of optimisations on that basis.
|
||||
*/
|
||||
export class Reflector extends PairMapBase {
|
||||
/**
|
||||
|
@ -231,6 +236,21 @@ export class Reflector extends PairMapBase {
|
|||
if (s !== 26) {
|
||||
throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
|
||||
}
|
||||
const optMap = new Array(26);
|
||||
for (const x of Object.keys(this.map)) {
|
||||
optMap[x] = this.map[x];
|
||||
}
|
||||
this.map = optMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this object.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(c) {
|
||||
return this.map[c];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
230
src/core/lib/LoremIpsum.mjs
Normal file
230
src/core/lib/LoremIpsum.mjs
Normal file
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* Lorem Ipsum generator.
|
||||
*
|
||||
* @author Klaxon [klaxon@veyr.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum paragraphs.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateParagraphs(length=3) {
|
||||
const paragraphs = [];
|
||||
while (paragraphs.length < length) {
|
||||
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
|
||||
const sentences = [];
|
||||
while (sentences.length < paragraphLength) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
const sentence = getWords(sentenceLength);
|
||||
sentences.push(formatSentence(sentence));
|
||||
}
|
||||
paragraphs.push(formatParagraph(sentences));
|
||||
}
|
||||
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -2);
|
||||
paragraphs[0] = replaceStart(paragraphs[0]);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum sentences.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateSentences(length=3) {
|
||||
const sentences = [];
|
||||
while (sentences.length < length) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
const sentence = getWords(sentenceLength);
|
||||
sentences.push(formatSentence(sentence));
|
||||
}
|
||||
const paragraphs = sentencesToParagraphs(sentences);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum words.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateWords(length=3) {
|
||||
const words = getWords(length);
|
||||
const sentences = wordsToSentences(words);
|
||||
const paragraphs = sentencesToParagraphs(sentences);
|
||||
return paragraphs.join("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate lorem ipsum bytes.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function GenerateBytes(length=3) {
|
||||
const str = GenerateWords(length/3);
|
||||
return str.slice(0, length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get array of randomly selected words from the lorem ipsum wordList.
|
||||
*
|
||||
* @param {number} length
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function getWords(length=3) {
|
||||
const words = [];
|
||||
let word;
|
||||
let previousWord;
|
||||
while (words.length < length){
|
||||
do {
|
||||
word = wordList[Math.floor(Math.random() * wordList.length)];
|
||||
} while (previousWord === word);
|
||||
words.push(word);
|
||||
previousWord = word;
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an array of words into an array of sentences
|
||||
*
|
||||
* @param {string[]} words
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function wordsToSentences(words) {
|
||||
const sentences = [];
|
||||
while (words.length > 0) {
|
||||
const sentenceLength = getRandomLength(SENTENCE_LENGTH_MEAN, SENTENCE_LENGTH_STD_DEV);
|
||||
if (sentenceLength <= words.length) {
|
||||
sentences.push(formatSentence(words.splice(0, sentenceLength)));
|
||||
} else {
|
||||
sentences.push(formatSentence(words.splice(0, words.length)));
|
||||
}
|
||||
}
|
||||
return sentences;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert an array of sentences into an array of paragraphs
|
||||
*
|
||||
* @param {string[]} sentences
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function sentencesToParagraphs(sentences) {
|
||||
const paragraphs = [];
|
||||
while (sentences.length > 0) {
|
||||
const paragraphLength = getRandomLength(PARAGRAPH_LENGTH_MEAN, PARAGRAPH_LENGTH_STD_DEV);
|
||||
paragraphs.push(formatParagraph(sentences.splice(0, paragraphLength)));
|
||||
}
|
||||
paragraphs[paragraphs.length-1] = paragraphs[paragraphs.length-1].slice(0, -1);
|
||||
paragraphs[0] = replaceStart(paragraphs[0]);
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an array of words into a sentence.
|
||||
*
|
||||
* @param {string[]} words
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function formatSentence(words) {
|
||||
// 0.35 chance of a comma being added randomly to the sentence.
|
||||
if (Math.random() < PROBABILITY_OF_A_COMMA) {
|
||||
const pos = Math.round(Math.random()*(words.length-1));
|
||||
words[pos] +=",";
|
||||
}
|
||||
let sentence = words.join(" ");
|
||||
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
||||
sentence += ".";
|
||||
return sentence;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an array of sentences into a paragraph.
|
||||
*
|
||||
* @param {string[]} sentences
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function formatParagraph(sentences) {
|
||||
let paragraph = sentences.join(" ");
|
||||
paragraph += "\n\n";
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a random number based on a mean and standard deviation.
|
||||
*
|
||||
* @param {number} mean
|
||||
* @param {number} stdDev
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
function getRandomLength(mean, stdDev) {
|
||||
let length;
|
||||
do {
|
||||
length = Math.round((Math.random()*2-1)+(Math.random()*2-1)+(Math.random()*2-1)*stdDev+mean);
|
||||
} while (length <= 0);
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace first 5 words with "Lorem ipsum dolor sit amet"
|
||||
*
|
||||
* @param {string[]} str
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
function replaceStart(str) {
|
||||
let words = str.split(" ");
|
||||
if (words.length > 5) {
|
||||
words.splice(0, 5, "Lorem", "ipsum", "dolor", "sit", "amet");
|
||||
return words.join(" ");
|
||||
} else {
|
||||
const lorem = ["Lorem", "ipsum", "dolor", "sit", "amet"];
|
||||
words = lorem.slice(0, words.length);
|
||||
str = words.join(" ");
|
||||
str += ".";
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const SENTENCE_LENGTH_MEAN = 15;
|
||||
const SENTENCE_LENGTH_STD_DEV = 9;
|
||||
const PARAGRAPH_LENGTH_MEAN = 5;
|
||||
const PARAGRAPH_LENGTH_STD_DEV = 2;
|
||||
const PROBABILITY_OF_A_COMMA = 0.35;
|
||||
|
||||
const wordList = [
|
||||
"ad", "adipisicing", "aliqua", "aliquip", "amet", "anim",
|
||||
"aute", "cillum", "commodo", "consectetur", "consequat", "culpa",
|
||||
"cupidatat", "deserunt", "do", "dolor", "dolore", "duis",
|
||||
"ea", "eiusmod", "elit", "enim", "esse", "est",
|
||||
"et", "eu", "ex", "excepteur", "exercitation", "fugiat",
|
||||
"id", "in", "incididunt", "ipsum", "irure", "labore",
|
||||
"laboris", "laborum", "Lorem", "magna", "minim", "mollit",
|
||||
"nisi", "non", "nostrud", "nulla", "occaecat", "officia",
|
||||
"pariatur", "proident", "qui", "quis", "reprehenderit", "sint",
|
||||
"sit", "sunt", "tempor", "ullamco", "ut", "velit",
|
||||
"veniam", "voluptate",
|
||||
];
|
Loading…
Add table
Add a link
Reference in a new issue