@ondoher/enigma 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/config.js +11 -0
- package/bin/enigma.js +125 -0
- package/bin/enigma.json +17 -0
- package/bin/instructions.txt +1 -0
- package/bin/types.d.ts +21 -0
- package/jsconfig.json +5 -2
- package/lib/enigma/Enigma.js +26 -12
- package/lib/enigma/EnigmaTypes.d.ts +1 -1
- package/lib/enigma/Rotor.js +32 -13
- package/lib/enigma/tests/RotorSpec.js +4 -4
- package/lib/utils/options.js +63 -0
- package/lib/utils/optionsTypes.d.ts +5 -0
- package/package.json +6 -4
package/README.md
CHANGED
package/bin/config.js
ADDED
package/bin/enigma.js
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import parseArgs from 'minimist';
|
|
2
|
+
import { getOptions } from '../lib/utils/options.js';
|
|
3
|
+
import { access, constants, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { CONFIG } from './config.js';
|
|
5
|
+
import Enigma from 'enigma/Enigma';
|
|
6
|
+
|
|
7
|
+
let argv = parseArgs(process.argv.slice(2));
|
|
8
|
+
let params = argv._;
|
|
9
|
+
let instructionText = '';
|
|
10
|
+
|
|
11
|
+
/** @type {Options} */
|
|
12
|
+
let defaultOptions = {
|
|
13
|
+
file: './enigma.json',
|
|
14
|
+
overwrite: false,
|
|
15
|
+
events: '',
|
|
16
|
+
step: false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @type {OptionsNameMap} */
|
|
20
|
+
let nameMap = {
|
|
21
|
+
file: 'f',
|
|
22
|
+
overwrite: 'o',
|
|
23
|
+
events: 'e',
|
|
24
|
+
step: 's'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/** @type {Options} */
|
|
29
|
+
let options = getOptions(defaultOptions, argv, nameMap);
|
|
30
|
+
let command = params[0];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Call this function to check if a file exists
|
|
34
|
+
*
|
|
35
|
+
* @param {String} name
|
|
36
|
+
* @returns {Promise<boolean>}
|
|
37
|
+
*/
|
|
38
|
+
async function fileExists(name) {
|
|
39
|
+
try {
|
|
40
|
+
await access(name, constants.R_OK | constants.W_OK);
|
|
41
|
+
return true;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Call this method to see if the config file exists
|
|
49
|
+
* @returns
|
|
50
|
+
*/
|
|
51
|
+
async function configExists() {
|
|
52
|
+
return await fileExists(options.file)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
*
|
|
57
|
+
* @returns {Promise<EnigmaConfig>}
|
|
58
|
+
*/
|
|
59
|
+
async function readConfig() {
|
|
60
|
+
if (!configExists) {
|
|
61
|
+
return CONFIG;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return JSON.parse(await readFile( options.file, 'utf-8'));
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function instructions() {
|
|
69
|
+
console.log(instructionText);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
*
|
|
74
|
+
* @param {string} str
|
|
75
|
+
*/
|
|
76
|
+
function error(str) {
|
|
77
|
+
console.log(str);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function init() {
|
|
81
|
+
if (await configExists() && !options.overwrite) {
|
|
82
|
+
error("cannot overwrite config file")
|
|
83
|
+
instructions();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await writeFile(options.file, JSON.stringify(CONFIG, null, " "), 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runEnigma() {
|
|
91
|
+
let config = await readConfig();
|
|
92
|
+
if (params.length > 1) {
|
|
93
|
+
config.encode = params[1];
|
|
94
|
+
}
|
|
95
|
+
let enigmaConfig = config.enigma;
|
|
96
|
+
if (!config.encode) {
|
|
97
|
+
error('no string to encode');
|
|
98
|
+
instructions();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let { name, rotors, ringSettings, reflector, plugs} = enigmaConfig;
|
|
102
|
+
let enigma = new Enigma(name, {reflector});
|
|
103
|
+
enigma.configure({rotors, ringSettings, plugs})
|
|
104
|
+
let decoded = enigma.translate("", config.encode)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
instructionText = await readFile('./instructions.txt', 'utf-8');
|
|
109
|
+
if (!command) {
|
|
110
|
+
instructions();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
switch (command) {
|
|
115
|
+
case "init":
|
|
116
|
+
await init();
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
instructions();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await main();
|
package/bin/enigma.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
silly boy
|
package/bin/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
file?: string;
|
|
5
|
+
overwrite?: boolean;
|
|
6
|
+
events?: string;
|
|
7
|
+
step?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type EnigmaConfig = {
|
|
11
|
+
enigma: {
|
|
12
|
+
name: string;
|
|
13
|
+
reflector: string;
|
|
14
|
+
rotors: string[],
|
|
15
|
+
ringSettings: number[] | string;
|
|
16
|
+
plugs: string | string[];
|
|
17
|
+
},
|
|
18
|
+
rotor?: string,
|
|
19
|
+
options?: Options,
|
|
20
|
+
encode?: string,
|
|
21
|
+
}
|
package/jsconfig.json
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "es2022", // Or a newer version like "es2020"
|
|
4
4
|
"module": "es2022",
|
|
5
|
-
"checkJs": true
|
|
6
|
-
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"baseUrl": "./",
|
|
7
|
+
"paths": {
|
|
8
|
+
"*": ["node_modules/@types/*", "*"]
|
|
9
|
+
} },
|
|
7
10
|
"exclude": [
|
|
8
11
|
"node_modules"
|
|
9
12
|
],
|
package/lib/enigma/Enigma.js
CHANGED
|
@@ -127,12 +127,35 @@ export default class Enigma extends Encoder {
|
|
|
127
127
|
* @public
|
|
128
128
|
*/
|
|
129
129
|
step() {
|
|
130
|
+
// Only the notches on the first two rotors effect stepping. One rotor's
|
|
131
|
+
// alphabet ring prevents the next rotor from turning unless the first
|
|
132
|
+
// rotor's notch is exposed. When a rotor does step, so does the
|
|
133
|
+
// previous rotor because they are attached by the notch in the
|
|
134
|
+
// previous rotor's ring
|
|
135
|
+
|
|
136
|
+
// precalculate if and why a rotor should step, the first rotor is
|
|
137
|
+
// always engaged
|
|
138
|
+
/** @type {{engaged:boolean, nextEngaged: boolean}[]} */
|
|
139
|
+
let step = []
|
|
140
|
+
|
|
141
|
+
step[0] = {engaged: true, nextEngaged: false};
|
|
142
|
+
|
|
143
|
+
for (let idx = 1; idx < 3; idx++) {
|
|
144
|
+
let engaged = this._rotors[idx - 1].atTurnover();
|
|
145
|
+
let nextEngaged = idx < 2 && this._rotors[idx].atTurnover();
|
|
146
|
+
|
|
147
|
+
step[idx] = {engaged, nextEngaged};
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
this._rotors.forEach((rotor, idx) => {
|
|
131
151
|
if (rotor.isFixed()) return;
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
if (step[idx].engaged || step[idx].nextEngaged) {
|
|
154
|
+
rotor.step();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// the double-step is caused by the the next rotor stepping
|
|
158
|
+
if (step[idx].nextEngaged) {
|
|
136
159
|
/** @type {EventData} */
|
|
137
160
|
let eventData = {
|
|
138
161
|
name: rotor.name,
|
|
@@ -141,18 +164,9 @@ export default class Enigma extends Encoder {
|
|
|
141
164
|
event: "double-step",
|
|
142
165
|
offset: rotor.offset,
|
|
143
166
|
}
|
|
144
|
-
|
|
145
167
|
this.fire('double-step', rotor.name, eventData);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (this.pending[idx]) {
|
|
149
|
-
this.pending[idx] = false;
|
|
150
|
-
if (rotor.step()) this.pending[idx + 1] = true;
|
|
151
168
|
}
|
|
152
169
|
});
|
|
153
|
-
|
|
154
|
-
// The first rotor is always stepping
|
|
155
|
-
this.pending[0] = true;
|
|
156
170
|
}
|
|
157
171
|
|
|
158
172
|
/**
|
package/lib/enigma/Rotor.js
CHANGED
|
@@ -5,6 +5,12 @@ import Encoder from "./Encoder.js";
|
|
|
5
5
|
* Create an instance of this class to construct a Rotor object. The Rotor class
|
|
6
6
|
* encapsulates many of the peculiar behaviors of the Enigma. All connector
|
|
7
7
|
* values here are specified in physical space.
|
|
8
|
+
*
|
|
9
|
+
* There are three positioning systems to keep in mind. There is the logical
|
|
10
|
+
* position that map an input to an output as defined by the wiring. There is
|
|
11
|
+
* the physical position of the rotor in space as related to the location of the
|
|
12
|
+
* connectors, the rotation offset. And there is an additional adjustment, the
|
|
13
|
+
* ring setting, that moves the internal wiring against the rotational position.
|
|
8
14
|
*/
|
|
9
15
|
export default class Rotor extends Encoder {
|
|
10
16
|
/**
|
|
@@ -18,6 +24,7 @@ export default class Rotor extends Encoder {
|
|
|
18
24
|
super(name, "Rotor", settings);
|
|
19
25
|
let { map = STANDARD_ALPHABET, turnovers = '', ringSetting = 0} = settings
|
|
20
26
|
|
|
27
|
+
this._settings = structuredClone(settings);
|
|
21
28
|
this.map = [...map];
|
|
22
29
|
this.rightMap = this.makeMap(map);
|
|
23
30
|
this.leftMap = this.makeReverseMap(this.rightMap);
|
|
@@ -36,6 +43,17 @@ export default class Rotor extends Encoder {
|
|
|
36
43
|
}, turnoverSet);
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @returns {RotorSetup}
|
|
48
|
+
*/
|
|
49
|
+
get settings() {
|
|
50
|
+
return this._settings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// get offset() {
|
|
54
|
+
// return this._offset
|
|
55
|
+
// }
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Call this method to select the initial rotation of the rotor. This is a
|
|
41
59
|
* letter offset from the logical 0 connector. The initial rotation will
|
|
@@ -83,13 +101,19 @@ export default class Rotor extends Encoder {
|
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/**
|
|
86
|
-
* Call this method to step the rotor
|
|
104
|
+
* Call this method to step the rotor. This is only responsible for updating
|
|
105
|
+
* the internal state. Double stepping is done by the enigma class
|
|
87
106
|
*
|
|
88
107
|
* @public
|
|
89
|
-
*
|
|
90
|
-
* @returns {Boolean} true if the next rotor should be stepped
|
|
91
108
|
*/
|
|
92
109
|
step() {
|
|
110
|
+
// let start = this.offset;
|
|
111
|
+
|
|
112
|
+
// // do the step
|
|
113
|
+
// this.offset = this.normalize(this.offset + 1);
|
|
114
|
+
|
|
115
|
+
// let stop = this.offset;
|
|
116
|
+
|
|
93
117
|
// Because the turnover notch is attached to the outer ring, and the
|
|
94
118
|
// logical coordinates are based on the wiring, the logical position of
|
|
95
119
|
// the notch for turnover needs to be adjusted to remove the ring offset.
|
|
@@ -111,26 +135,21 @@ export default class Rotor extends Encoder {
|
|
|
111
135
|
turnover, start, stop
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
// console.log("firing", eventData);
|
|
115
138
|
this.fire('step', this.name, eventData);
|
|
116
139
|
|
|
117
140
|
return turnover;
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
/**
|
|
121
|
-
* Call this method to see if the
|
|
122
|
-
* turnover. The Enigma class will call this on the middle rotor to handle
|
|
123
|
-
* double stepping.
|
|
144
|
+
* Call this method to see if the router turnover notch is exposed.
|
|
124
145
|
*
|
|
125
146
|
* @public
|
|
126
147
|
*
|
|
127
|
-
* @returns true if
|
|
148
|
+
* @returns true if it is, false otherwise
|
|
128
149
|
*/
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
// double stepping happens when we step to the turnover point
|
|
133
|
-
return this._turnovers.has(turnoverOffset);
|
|
150
|
+
atTurnover() {
|
|
151
|
+
let currentOffset = this.normalize(this.offset + this.ringOffset);
|
|
152
|
+
return this._turnovers.has(currentOffset);
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
|
|
@@ -11,7 +11,7 @@ describe('Rotor Test Cases', function() {
|
|
|
11
11
|
map: rotorData.createData.map,
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
expect(rotor
|
|
14
|
+
expect(rotor["leftMap"]).toEqual(rotorData.createData.reverseMap)
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('sets the length to the map length', function() {
|
|
@@ -20,7 +20,7 @@ describe('Rotor Test Cases', function() {
|
|
|
20
20
|
map: rotorData.createData.map,
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
expect(rotor
|
|
23
|
+
expect(rotor["length"]).toEqual(rotorData.createData.map.length)
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
it('remembers multiple turnovers', function() {
|
|
@@ -88,7 +88,7 @@ describe('Rotor Test Cases', function() {
|
|
|
88
88
|
|
|
89
89
|
it('handles double step', function() {
|
|
90
90
|
rotor.setStartPosition('Q');
|
|
91
|
-
let result = rotor.
|
|
91
|
+
let result = rotor.atTurnover();
|
|
92
92
|
expect(result).toBe(true);
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -115,7 +115,7 @@ describe('Rotor Test Cases', function() {
|
|
|
115
115
|
it('handles double step', function() {
|
|
116
116
|
rotor.ringOffset = 1;
|
|
117
117
|
rotor.setStartPosition('Q');
|
|
118
|
-
let result = rotor.
|
|
118
|
+
let result = rotor.atTurnover();
|
|
119
119
|
expect(result).toBe(true);
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as minimist from "minimist";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Call this method to try to convert the input value to a
|
|
5
|
+
* boolean value, if not the return result will be the value passed in
|
|
6
|
+
*
|
|
7
|
+
* @param {string | number | boolean} val - the value to check
|
|
8
|
+
* @returns {string | boolean | number} the converted boolean or the input value
|
|
9
|
+
*/
|
|
10
|
+
function checkBoolean(val) {
|
|
11
|
+
let boolValues = {
|
|
12
|
+
'true' : true,
|
|
13
|
+
'false': false,
|
|
14
|
+
'on': true,
|
|
15
|
+
'off': false,
|
|
16
|
+
'set': true,
|
|
17
|
+
'clear': false,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (val === true) return true;
|
|
21
|
+
if (val === false) return false;
|
|
22
|
+
|
|
23
|
+
return boolValues[val] !== undefined ? boolValues[val] : val;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Call this method to get the value of an option from the cli. It will convert
|
|
28
|
+
* this value to a boolean if it can.
|
|
29
|
+
*
|
|
30
|
+
* @param {minimist.ParsedArgs} argv - the parsed command line
|
|
31
|
+
* @param {String} name - the full name of the option
|
|
32
|
+
* @param {String} short - the short name of the optio
|
|
33
|
+
* @returns {OptionsValue} the value of the option
|
|
34
|
+
*/
|
|
35
|
+
export function getOption(argv, name, short) {
|
|
36
|
+
if (argv[name] !== undefined) return checkBoolean(argv[name]);
|
|
37
|
+
if (argv[short] != undefined) return checkBoolean(argv[short]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Call this method to parse all the command line options, those
|
|
42
|
+
* that start with either - or --.
|
|
43
|
+
*
|
|
44
|
+
* @param {OptionValues} defaults - The default values, these will be returned if they are unchanged
|
|
45
|
+
* @param {minimist.ParsedArgs} argv - the parsed command line
|
|
46
|
+
* @param {OptionsNameMap} nameMap - a mapping between the full name and the short name
|
|
47
|
+
* @returns {OptionValues} the parsed options
|
|
48
|
+
*/
|
|
49
|
+
export function getOptions(defaults, argv, nameMap) {
|
|
50
|
+
let longNames = Object.keys(nameMap);
|
|
51
|
+
let options = structuredClone(defaults);
|
|
52
|
+
|
|
53
|
+
options = longNames.reduce(function(options, long) {
|
|
54
|
+
let short = nameMap[long];
|
|
55
|
+
let option = getOption(argv, long, short);
|
|
56
|
+
|
|
57
|
+
options[long] = option ?? options[long];
|
|
58
|
+
|
|
59
|
+
return options;
|
|
60
|
+
}, options);
|
|
61
|
+
|
|
62
|
+
return options;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ondoher/enigma",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Enigma machine simulator with tools and documentation to help you build your own.",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
"homepage": "https://github.com/Ondoher/enigma#readme",
|
|
28
28
|
"type": "module",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^25.0.10",
|
|
31
30
|
"@types/jasmine": "^4.6.6",
|
|
32
|
-
"
|
|
31
|
+
"@types/node": "^25.0.10",
|
|
33
32
|
"jasmine": "^4.5.0",
|
|
34
|
-
"jasmine-console-reporter": "^3.1.0"
|
|
33
|
+
"jasmine-console-reporter": "^3.1.0",
|
|
34
|
+
"undici-types": "^7.19.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@types/minimist": "^1.2.5",
|
|
38
|
+
"minimist": "^1.2.8"
|
|
37
39
|
}
|
|
38
40
|
}
|