1 // Written in the D programming language.
2 
3 /**
4  * A library for simulating the Enigma machines.
5  *
6  * Copyright: Copyright Kazuya Takahashi 2016.
7  * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
8  * Authors:   Kazuya Takahashi
9  */
10 module enigma;
11 
12 private enum size_t N = 26;
13 
14 private template isSomeStringOrDcharRange(T)
15 {
16     import std.range.primitives : isInfinite, isInputRange, ElementType;
17     import std.traits : isSomeString;
18 
19     enum isSomeStringOrDcharRange = isSomeString!T ||
20         (isInputRange!T && !isInfinite!T && is(ElementType!T : dchar));
21 }
22 
23 
24 ///
25 struct Rotor
26 {
27     import boolean_matrix : BSM;
28 
29     immutable BSM!N perm;
30     private immutable bool hasNotch = false;
31     private immutable size_t[] turnovers;
32 
33     import std.meta : allSatisfy;
34     import std.traits : isIntegral;
35     /++
36      + Constructs a rotor.
37      + If turnovers are not specified, the rotor has no turnover notches,
38      + otherwise, for example, if turnovers are `1` and `25`, the next rotor
39      + steps when this rotor steps from B to C and from Z to A.
40      + If ringOffset is `2`, it corresponds to "C-03".
41      +/
42     this(I...)(in auto ref BSM!N perm, I turnovers, size_t ringOffset) pure
43         if (allSatisfy!(isIntegral, I) && I.length <= N)
44     in
45     {
46         import boolean_matrix : isBijective;
47 
48         assert(perm.isBijective, "Rotor must be bijective.");
49         foreach (t; turnovers)
50         {
51             assert(t >= 0, "Turnover must be positive.");
52         }
53     }
54     body
55     {
56         import std.algorithm.iteration : map, uniq;
57         import std.algorithm.sorting : sort;
58         import std.array : array;
59         import boolean_matrix : cyclicPermutation, cyclicPermutationInv;
60 
61         this.perm = cyclicPermutationInv!N(ringOffset) * perm * cyclicPermutation!N(ringOffset);
62         size_t[] ts = [turnovers];
63         this.turnovers = ts.map!(a => a % N).array.sort().uniq.array.idup;
64         hasNotch = I.length > 0;
65     }
66 }
67 
68 import std.meta : allSatisfy;
69 import std.traits : isSomeChar;
70 /++
71  + A convenience function to make a rotor from a forward substitution.
72  + If turnovers are not specified, the rotor has no turnover notches,
73  + otherwise, for example, if  turnovers are `'B'` and `'Z'`, the next rotor
74  + steps when this rotor steps from B to C and from Z to A.
75  + If ringSetting is `'C'`, it corresponds to "C-03".
76  +/
77 auto rotor(S, C...)(in S forwardSubstitution, C turnovers, dchar ringSetting) pure
78     if (isSomeStringOrDcharRange!S && allSatisfy!(isSomeChar, C) && C.length <= N)
79 in
80 {
81     import std.algorithm.comparison : isPermutation;
82     import std.algorithm.iteration : map;
83     import std.ascii : isAlpha, toUpper;
84     import std.range : iota, walkLength;
85 
86     assert(forwardSubstitution.walkLength == N, "Bad length.");
87     assert(N.iota.isPermutation(forwardSubstitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
88     foreach (dchar t; turnovers)
89     {
90         assert(t.isAlpha, "Bad turnover setting.");
91     }
92     assert(ringSetting.isAlpha, "Bad ring setting.");
93 }
94 body
95 {
96     import std.algorithm.iteration : map;
97     import std.array : array;
98     import std.ascii : toUpper;
99     import boolean_matrix : permutation;
100 
101     static if (C.length)
102     {
103         import std.typecons : Tuple;
104         import meta_workaround : Repeat;
105 
106         Tuple!(Repeat!(C.length, size_t)) ts;
107         foreach (i, dchar t; turnovers)
108         {
109             ts[i] = t.toUpper - 'A';
110         }
111         return Rotor(forwardSubstitution.map!toUpper.map!"a-size_t('A')".array.permutation!N,
112             ts.expand, ringSetting.toUpper - 'A');
113     }
114     else
115     {
116         return Rotor(forwardSubstitution.map!toUpper.map!"a-size_t('A')".array.permutation!N,
117             ringSetting.toUpper - 'A');
118     }
119 }
120 
121 ///
122 struct EntryWheel
123 {
124     import boolean_matrix : BSM;
125 
126     immutable BSM!N perm;
127     ///
128     this()(in auto ref BSM!N perm) pure
129     in
130     {
131         import boolean_matrix : isBijective;
132 
133         assert(perm.isBijective, "Entry wheel must be bijective.");
134     }
135     body
136     {
137         this.perm = cast(immutable) perm;
138     }
139 
140     alias perm this;
141 }
142 
143 /++
144  + A convenience function to make an entry wheel from a substitution.
145  +/
146 auto entryWheel(S)(in S backwardSubstitution) pure if (isSomeStringOrDcharRange!S)
147 in
148 {
149     import std.algorithm.comparison : isPermutation;
150     import std.algorithm.iteration : map;
151     import std.ascii : toUpper;
152     import std.range : iota, walkLength;
153 
154     assert(backwardSubstitution.walkLength == N, "Bad length.");
155     assert(N.iota.isPermutation(backwardSubstitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
156 }
157 body
158 {
159     import std.algorithm.iteration : map;
160     import std.array : array;
161     import std.ascii : toUpper;
162     import boolean_matrix : permutation, transpose;
163 
164     return EntryWheel(backwardSubstitution.map!toUpper.map!"a-size_t('A')".array.permutation!N.transpose);
165 }
166 
167 ///
168 struct Plugboard
169 {
170     import boolean_matrix : BSM;
171 
172     immutable BSM!N perm;
173     ///
174     this()(in auto ref BSM!N perm) pure
175     in
176     {
177         import boolean_matrix : isBijective, isSymmetric;
178 
179         assert(perm.isBijective, "Plugboard must be bijective.");
180         assert(perm.isSymmetric, "Plugboard must be symmetric.");
181     }
182     body
183     {
184         this.perm = cast(immutable) perm;
185     }
186 
187     alias perm this;
188 }
189 
190 /++
191  + A convenience function to make a plugboard from a substitution.
192  +/
193 auto plugboard(S)(in S substitution) pure if (isSomeStringOrDcharRange!S)
194 in
195 {
196     import std.algorithm.comparison : isPermutation;
197     import std.algorithm.iteration : map;
198     import std.ascii : toUpper;
199     import std.range : iota, walkLength;
200 
201     assert(substitution.walkLength == N, "Bad length.");
202     assert(N.iota.isPermutation(substitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
203 }
204 body
205 {
206     import std.algorithm.iteration : map;
207     import std.array : array;
208     import std.ascii : toUpper;
209     import boolean_matrix : permutation;
210 
211     return Plugboard(substitution.map!toUpper.map!"a-size_t('A')".array.permutation!N);
212 }
213 
214 ///
215 struct Reflector
216 {
217     import boolean_matrix : BSM;
218 
219     immutable BSM!N perm;
220     ///
221     this()(in auto ref BSM!N perm, size_t ringOffset) pure
222     in
223     {
224         import boolean_matrix : isBijective, isIrreflexive, isSymmetric;
225 
226         assert(perm.isBijective, "Reflector must be bijective.");
227         assert(perm.isIrreflexive, "Reflector must be irreflexive.");
228         assert(perm.isSymmetric, "Reflector must be bijective.");
229     }
230     body
231     {
232         import boolean_matrix : cyclicPermutation, cyclicPermutationInv;
233 
234         this.perm = cyclicPermutationInv!N(ringOffset) * perm * cyclicPermutation!N(ringOffset);
235     }
236 
237     alias perm this;
238 }
239 
240 /++
241  + A convenience function to make a reflector from a substitution.
242  +/
243 auto reflector(S)(in S substitution, dchar ringSetting = 'A') pure if (isSomeStringOrDcharRange!S)
244 in
245 {
246     import std.algorithm.comparison : isPermutation;
247     import std.algorithm.iteration : map;
248     import std.algorithm.searching : canFind;
249     import std.ascii : isAlpha, toUpper;
250     import std.range : iota, walkLength, zip;
251 
252     assert(substitution.walkLength == N, "Bad length.");
253     assert(!N.iota.zip(substitution.map!toUpper.map!"a-'A'").canFind!"a[0]==a[1]", "Self-loop found.");
254     assert(N.iota.isPermutation(substitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
255     assert(ringSetting.isAlpha, "Bad ring setting.");
256 }
257 body
258 {
259     import std.algorithm.iteration : map;
260     import std.array : array;
261     import std.ascii : toUpper;
262     import boolean_matrix : permutation;
263 
264     return Reflector(substitution.map!toUpper.map!"a-size_t('A')".array.permutation!N,
265         ringSetting.toUpper - 'A');
266 }
267 
268 ///
269 enum EnigmaType : uint
270 {
271     ///
272     none,
273     /// No double stepping.
274     gearDrive = 1 << 0,
275     ///
276     plugboard = 1 << 1,
277     ///
278     fixedFinalRotor = 1 << 2,
279     ///
280     settableReflectorPos = 1 << 3,
281     ///
282     movableReflector = 1 << 4
283 }
284 
285 ///
286 struct Enigma(size_t rotorN, EnigmaType enigmaType = EnigmaType.none)
287 {
288     import boolean_matrix : BSM;
289     private immutable BSM!N composedInputPerm;
290     private immutable Rotor[rotorN] rotors;
291     private immutable BSM!N reflector;
292     private size_t[rotorN + 1] rotationStates;
293 
294     import meta_workaround : Repeat;
295 
296     ///
297     this(in EntryWheel entryWheel, in Repeat!(rotorN, Rotor) rotors,
298         in Reflector reflector, in dchar[rotorN] rotorStartPos)
299     in
300     {
301         foreach (dchar c; rotorStartPos)
302         {
303             import std.ascii : isAlpha;
304 
305             assert(c.isAlpha, "Bad start position.");
306         }
307     }
308     body
309     {
310         foreach (i, ref e; rotorStartPos)
311         {
312             import std.ascii : toUpper;
313 
314             rotationStates[i] = e.toUpper - 'A';
315         }
316 
317         this.composedInputPerm = cast(immutable) entryWheel.perm;
318         this.rotors[] = cast(immutable)[rotors][];
319         this.reflector = cast(immutable) reflector.perm;
320     }
321 
322     ///
323     this()(in EntryWheel entryWheel, in Repeat!(rotorN, Rotor) rotors,
324             in Reflector reflector, in dchar[rotorN] rotorStartPos, dchar reflectorPos)
325             if (enigmaType & EnigmaType.settableReflectorPos
326                 || enigmaType & EnigmaType.movableReflector)
327     in
328     {
329         import std.ascii : isAlpha;
330 
331         assert(reflectorPos.isAlpha, "Bad reflector position.");
332     }
333     body
334     {
335         this(entryWheel, rotors, reflector, rotorStartPos);
336 
337         import std.ascii : toUpper;
338 
339         rotationStates[$ - 1] = reflectorPos.toUpper - 'A';
340     }
341 
342     ///
343     this()(in Plugboard plugboard, in EntryWheel entryWheel, in Repeat!(rotorN,
344             Rotor) rotors, in Reflector reflector, in dchar[rotorN] rotorStartPos)
345             if (enigmaType & EnigmaType.plugboard)
346     {
347         this(entryWheel, rotors, reflector, rotorStartPos);
348         this.composedInputPerm = cast(immutable)(entryWheel * plugboard);
349     }
350 
351     ///
352     this()(in Plugboard plugboard, in EntryWheel entryWheel, in Repeat!(rotorN, Rotor) rotors,
353             in Reflector reflector, in dchar[rotorN] rotorStartPos, dchar reflectorPos)
354             if (enigmaType & EnigmaType.plugboard && (enigmaType & EnigmaType.settableReflectorPos
355                 || enigmaType & EnigmaType.movableReflector))
356     in
357     {
358         import std.ascii : isAlpha;
359 
360         assert(reflectorPos.isAlpha, "Bad reflector position.");
361     }
362     body
363     {
364         this(plugboard, entryWheel, rotors, reflector, rotorStartPos);
365 
366         import std.ascii : toUpper;
367 
368         rotationStates[$ - 1] = reflectorPos.toUpper - 'A';
369     }
370 
371     private void step()
372     {
373         enum movableRotorN = (enigmaType & EnigmaType.fixedFinalRotor) ? rotorN - 1 :
374             (enigmaType & EnigmaType.movableReflector) ? rotorN + 1 : rotorN;
375         bool[movableRotorN] stepFlag;
376 
377         stepFlag[0] = true;
378 
379         foreach (rotorID; 0 .. movableRotorN - 1)
380         {
381             import std.algorithm.searching : canFind;
382 
383             if (rotors[rotorID].turnovers.canFind(rotationStates[rotorID]))
384             {
385                 stepFlag[rotorID] = true;
386                 stepFlag[rotorID + 1] = true;
387             }
388             else static if (enigmaType & EnigmaType.gearDrive)
389             {
390                 break;
391             }
392         }
393 
394         foreach (rotorID, e; stepFlag)
395         {
396             if (e)
397             {
398                 rotationStates[rotorID] = (rotationStates[rotorID] + 1) % N;
399             }
400             else static if (enigmaType & EnigmaType.gearDrive)
401             {
402                 break;
403             }
404         }
405     }
406 
407     import boolean_matrix : BCV;
408 
409     /+
410      + fwdPerm = (Um*Rm*Lm)*(Um-1*Rm-1*Lm-1)*...*(U0*R0*L0)*P
411      +         = Um*(Rm*Lm*Um-1)*(Rm-1*Lm-1*Um-2)*...*(R0*L0)*P
412      +         = Um*(Rm*RELm)*(Rm-1*RELm-1)*...*(R0*L0)*P
413      +/
414     private BCV!N composeForward(in ref BCV!N inputVec, size_t rotorID)
415     {
416         import boolean_matrix : cyclicPermutation, cyclicPermutationInv;
417 
418         immutable ptrdiff_t x = rotorID == 0 ? rotationStates[0] : rotationStates[rotorID] - rotationStates[rotorID - 1];
419         immutable relRotator = x > 0 ? cyclicPermutationInv!N(x) : cyclicPermutation!N(-x);
420         immutable composedVec = rotors[rotorID].perm * (relRotator * inputVec);
421         return rotorID == rotorN - 1 ? cyclicPermutation!N(rotationStates[rotorID]) * composedVec
422             : composeForward(composedVec, rotorID + 1);
423     }
424 
425     /+
426      + bwdPerm = [(Um*Rm*Lm)*(Um-1*Rm-1*Lm-1)*...*(U0*R0*L0)*P]^-1
427      +         = [Um*(Rm*RELm)*(Rm-1*RELm-1)*...*(R0*L0)*P]^-1
428      +         = P^-1*(R0*L0)^-1*...(Rm-1*RELm-1)^-1*(Rm*RELm)^-1*Um^-1
429      +         = P^-1*(U0*R0^-1)*...(RELm-1^-1*Rm-1^-1)*(RELm^-1*Rm^-1)*Lm
430      +/
431     private BCV!N composeBackward(in ref BCV!N inputVec, size_t rotorID)
432     {
433         import boolean_matrix : cyclicPermutation, cyclicPermutationInv, transpose;
434 
435         immutable ptrdiff_t x = rotorID == 0 ? rotationStates[0] : rotationStates[rotorID] - rotationStates[rotorID - 1];
436         immutable relRotatorInv = x > 0 ? cyclicPermutation!N(x) : cyclicPermutationInv!N(-x);
437         immutable iv = rotorID == rotorN - 1 ? cyclicPermutationInv!N(rotationStates[rotorN - 1]) * inputVec : inputVec;
438         immutable composedVec =  relRotatorInv * (rotors[rotorID].perm.transpose * iv);
439         return rotorID == 0 ? composedVec : composeBackward(composedVec, rotorID - 1);
440     }
441 
442     // Unless the reflector is movable, the return value is constant.
443     private auto composedReflector() @property
444     {
445         import boolean_matrix : cyclicPermutation, cyclicPermutationInv;
446 
447         return cyclicPermutation!N(rotationStates[$ - 1]) * reflector * cyclicPermutationInv!N(rotationStates[$ - 1]);
448     }
449 
450     private auto process(size_t keyInputID)
451     out (r)
452     {
453         assert(r >= 0);
454     }
455     body
456     {
457         step();
458 
459         import boolean_matrix : transpose, BCV;
460 
461         immutable composedInput = composedInputPerm * BCV!N.e(keyInputID);
462         immutable reflectorOutput = composedReflector * composeForward(composedInput, 0);
463         immutable composedOutput = composedInputPerm.transpose * composeBackward(reflectorOutput,rotorN - 1) ;
464         import std.algorithm.searching : countUntil;
465 
466         immutable r = composedOutput[].countUntil!"a";
467         return r;
468     }
469 
470     /// Enciphers only an alphabetical character through the current Enigma machine.
471     dchar opCall(dchar keyInput)
472     {
473         import std.ascii : isAlpha, toUpper;
474 
475         return keyInput.isAlpha ? cast(dchar)process(keyInput.toUpper - 'A') + 'A' : keyInput;
476     }
477 }
478 
479 /// Enigma I 'Wehrmacht', which has three rotor slots.
480 alias EnigmaI = Enigma!(3, EnigmaType.plugboard);
481 
482 /// Enigma M3, which has three rotor slots.
483 alias EnigmaM3 = EnigmaI;
484 
485 /// Enigma M4, which has four rotor slots. The fourth rotor never rotates.
486 alias EnigmaM4 = Enigma!(4, EnigmaType.fixedFinalRotor | EnigmaType.plugboard);
487 
488 /// Norway Enigma, which has three rotor slots.
489 alias Norway = EnigmaI;
490 
491 /// Enigma D, which has three rotor slots and no plugboard. The reflector can be set to any positions.
492 alias EnigmaD = Enigma!(3, EnigmaType.settableReflectorPos);
493 
494 /// Swiss K, which has three rotor slots and no plugboard. The reflector can be set to any positions.
495 alias SwissK = EnigmaD;
496 
497 /// Enigma R, which has three rotor slots and no plugboard. The reflector can be set to any positions.
498 alias EnigmaR = EnigmaD;
499 
500 /// Enigma T 'Tirpitz', which has three rotor slots and no plugboard. The reflector can be set to any positions.
501 alias EnigmaT = EnigmaD;
502 
503 /// Enigma KD, which has three rotor slots and no plugboard. The reflector can be set to any positions.
504 alias EnigmaKD = EnigmaD;
505 
506 /// Enigma A28, which has three rotor slots and no plugboard. The reflector can be set to any positions. In addition, the rotors and the reflector rotates normally (not "double stepping").
507 alias EnigmaA28 = Enigma!(3, EnigmaType.gearDrive | EnigmaType.movableReflector);
508 
509 /// Enigma G, which has three rotor slots and no plugboard. The reflector can be set to any positions. In addition, the rotors and the reflector rotates normally (not "double stepping").
510 alias EnigmaG = EnigmaA28;
511 
512 /// Predefined existent rotors.
513 auto rotorI(dchar ringSetting = 'A') pure
514 {
515     return rotor("EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'Q', ringSetting);
516 }
517 
518 /// ditto
519 auto rotorII(dchar ringSetting = 'A') pure
520 {
521     return rotor("AJDKSIRUXBLHWTMCQGZNPYFVOE", 'E', ringSetting);
522 }
523 
524 /// ditto
525 auto rotorIII(dchar ringSetting = 'A') pure
526 {
527     return rotor("BDFHJLCPRTXVZNYEIWGAKMUSQO", 'V', ringSetting);
528 }
529 
530 /// ditto
531 auto rotorIV(dchar ringSetting = 'A') pure
532 {
533     return rotor("ESOVPZJAYQUIRHXLNFTGKDCMWB", 'J', ringSetting);
534 }
535 
536 /// ditto
537 auto rotorV(dchar ringSetting = 'A') pure
538 {
539     return rotor("VZBRGITYUPSDNHLXAWMJQOFECK", 'Z', ringSetting);
540 }
541 
542 /// ditto
543 auto rotorVI(dchar ringSetting = 'A') pure
544 {
545     return rotor("JPGVOUMFYQBENHZRDKASXLICTW", 'Z', 'M', ringSetting);
546 }
547 
548 /// ditto
549 auto rotorVII(dchar ringSetting = 'A') pure
550 {
551     return rotor("NZJHGRCXMYSWBOUFAIVLPEKQDT", 'Z', 'M', ringSetting);
552 }
553 
554 /// ditto
555 auto rotorVIII(dchar ringSetting = 'A') pure
556 {
557     return rotor("FKQHTLXOCBJSPDZRAMEWNIUYGV", 'Z', 'M', ringSetting);
558 }
559 
560 /// ditto
561 auto rotorINor(dchar ringSetting = 'A') pure
562 {
563     return rotor("WTOKASUYVRBXJHQCPZEFMDINLG", 'Q', ringSetting);
564 }
565 
566 /// ditto
567 auto rotorIINor(dchar ringSetting = 'A') pure
568 {
569     return rotor("GJLPUBSWEMCTQVHXAOFZDRKYNI", 'E', ringSetting);
570 }
571 
572 /// ditto
573 auto rotorIIINor(dchar ringSetting = 'A') pure
574 {
575     return rotor("JWFMHNBPUSDYTIXVZGRQLAOEKC", 'V', ringSetting);
576 }
577 
578 /// ditto
579 alias rotorIVNor = rotorIV;
580 
581 /// ditto
582 auto rotorVNor(dchar ringSetting = 'A') pure
583 {
584     return rotor("HEJXQOTZBVFDASCILWPGYNMURK", 'Z', ringSetting);
585 }
586 
587 /// ditto
588 auto rotorID(dchar ringSetting = 'A') pure
589 {
590     return rotor("LPGSZMHAEOQKVXRFYBUTNICJDW", 'Y', ringSetting);
591 }
592 
593 /// ditto
594 auto rotorIID(dchar ringSetting = 'A') pure
595 {
596     return rotor("SLVGBTFXJQOHEWIRZYAMKPCNDU", 'E', ringSetting);
597 }
598 
599 /// ditto
600 auto rotorIIID(dchar ringSetting = 'A') pure
601 {
602     return rotor("CJGDPSHKTURAWZXFMYNQOBVLIE", 'N', ringSetting);
603 }
604 
605 /// ditto
606 auto rotorIK(dchar ringSetting = 'A') pure
607 {
608     return rotor("PEZUOHXSCVFMTBGLRINQJWAYDK", 'Y', ringSetting);
609 }
610 
611 /// ditto
612 auto rotorIIK(dchar ringSetting = 'A') pure
613 {
614     return rotor("ZOUESYDKFWPCIQXHMVBLGNJRAT", 'E', ringSetting);
615 }
616 
617 /// ditto
618 auto rotorIIIK(dchar ringSetting = 'A') pure
619 {
620     return rotor("EHRVXGAOBQUSIMZFLYNWKTPDJC", 'N', ringSetting);
621 }
622 
623 /// ditto
624 auto rotorIR(dchar ringSetting = 'A') pure
625 {
626     return rotor("JGDQOXUSCAMIFRVTPNEWKBLZYH", 'N', ringSetting);
627 }
628 
629 /// ditto
630 auto rotorIIR(dchar ringSetting = 'A') pure
631 {
632     return rotor("NTZPSFBOKMWRCJDIVLAEYUXHGQ", 'E', ringSetting);
633 }
634 
635 /// ditto
636 auto rotorIIIR(dchar ringSetting = 'A') pure
637 {
638     return rotor("JVIUBHTCDYAKEQZPOSGXNRMWFL", 'Y', ringSetting);
639 }
640 
641 /// ditto
642 auto rotorIT(dchar ringSetting = 'A') pure
643 {
644     return rotor("KPTYUELOCVGRFQDANJMBSWHZXI", 'W', 'Z', 'E', 'K', 'Q', ringSetting);
645 }
646 
647 /// ditto
648 auto rotorIIT(dchar ringSetting = 'A') pure
649 {
650     return rotor("UPHZLWEQMTDJXCAKSOIGVBYFNR", 'W', 'Z', 'F', 'L', 'R', ringSetting);
651 }
652 
653 /// ditto
654 auto rotorIIIT(dchar ringSetting = 'A') pure
655 {
656     return rotor("QUDLYRFEKONVZAXWHMGPJBSICT", 'W', 'Z', 'E', 'K', 'Q', ringSetting);
657 }
658 
659 /// ditto
660 auto rotorIVT(dchar ringSetting = 'A') pure
661 {
662     return rotor("CIWTBKXNRESPFLYDAGVHQUOJZM", 'W', 'Z', 'F', 'L', 'R', ringSetting);
663 }
664 
665 /// ditto
666 auto rotorVT(dchar ringSetting = 'A') pure
667 {
668     return rotor("UAXGISNJBVERDYLFZWTPCKOHMQ", 'Y', 'C', 'F', 'K', 'R', ringSetting);
669 }
670 
671 /// ditto
672 auto rotorVIT(dchar ringSetting = 'A') pure
673 {
674     return rotor("XFUZGALVHCNYSEWQTDMRBKPIOJ", 'X', 'E', 'I', 'M', 'Q', ringSetting);
675 }
676 
677 /// ditto
678 auto rotorVIIT(dchar ringSetting = 'A') pure
679 {
680     return rotor("BJVFTXPLNAYOZIKWGDQERUCHSM", 'Y', 'C', 'F', 'K', 'R', ringSetting);
681 }
682 
683 /// ditto
684 auto rotorVIIIT(dchar ringSetting = 'A') pure
685 {
686     return rotor("YMTPNZHWKODAJXELUQVGCBISFR", 'X', 'E', 'I', 'M', 'Q', ringSetting);
687 }
688 
689 /// ditto
690 auto rotorIKD(dchar ringSetting = 'A') pure
691 {
692     return rotor("VEZIOJCXKYDUNTWAPLQGBHSFMR", 'S', 'U', 'Y', 'A', 'E', 'H', 'L', 'N', 'Q', ringSetting);
693 }
694 
695 /// ditto
696 auto rotorIIKD(dchar ringSetting = 'A') pure
697 {
698     return rotor("HGRBSJZETDLVPMQYCXAOKINFUW", 'S', 'U', 'Y', 'A', 'E', 'H', 'L', 'N', 'Q', ringSetting);
699 }
700 
701 /// ditto
702 auto rotorIIIKD(dchar ringSetting = 'A') pure
703 {
704     return rotor("NWLHXGRBYOJSAZDVTPKFQMEUIC", 'S', 'U', 'Y', 'A', 'E', 'H', 'L', 'N', 'Q', ringSetting);
705 }
706 
707 /// ditto
708 auto rotorIA865(dchar ringSetting = 'A') pure
709 {
710     return rotor("LPGSZMHAEOQKVXRFYBUTNICJDW", 'S', 'U', 'V', 'W', 'Z', 'A', 'B', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'O', 'P', 'Q', ringSetting);
711 }
712 
713 /// ditto
714 auto rotorIIA865(dchar ringSetting = 'A') pure
715 {
716     return rotor("SLVGBTFXJQOHEWIRZYAMKPCNDU", 'S', 'T', 'V', 'Y', 'Z', 'A', 'C', 'D', 'F', 'G', 'H', 'K', 'M', 'N', 'Q', ringSetting);
717 }
718 
719 /// ditto
720 auto rotorIIIA865(dchar ringSetting = 'A') pure
721 {
722     return rotor("CJGDPSHKTURAWZXFMYNQOBVLIE", 'U', 'W', 'X', 'A', 'E', 'F', 'H', 'K', 'M', 'N', 'R', ringSetting);
723 }
724 
725 /// ditto
726 auto rotorIG260(dchar ringSetting = 'A') pure
727 {
728     return rotor("RCSPBLKQAUMHWYTIFZVGOJNEXD", 'S', 'U', 'V', 'W', 'Z', 'A', 'B', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'O', 'P', 'Q', ringSetting);
729 }
730 
731 /// ditto
732 auto rotorIIG260(dchar ringSetting = 'A') pure
733 {
734     return rotor("WCMIBVPJXAROSGNDLZKEYHUFQT", 'S', 'T', 'V', 'Y', 'Z', 'A', 'C', 'D', 'F', 'G', 'H', 'K', 'M', 'N', 'Q', ringSetting);
735 }
736 
737 /// ditto
738 auto rotorIIIG260(dchar ringSetting = 'A') pure
739 {
740     return rotor("FVDHZELSQMAXOKYIWPGCBUJTNR", 'U', 'W', 'X', 'A', 'E', 'F', 'H', 'K', 'M', 'N', 'R', ringSetting);
741 }
742 
743 /// ditto
744 auto rotorIG312(dchar ringSetting = 'A') pure
745 {
746     return rotor("DMTWSILRUYQNKFEJCAZBPGXOHV", 'S', 'U', 'V', 'W', 'Z', 'A', 'B', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'O', 'P', 'Q', ringSetting);
747 }
748 
749 /// ditto
750 auto rotorIIG312(dchar ringSetting = 'A') pure
751 {
752     return rotor("HQZGPJTMOBLNCIFDYAWVEUSRKX", 'S', 'T', 'V', 'Y', 'Z', 'A', 'C', 'D', 'F', 'G', 'H', 'K', 'M', 'N', 'Q', ringSetting);
753 }
754 
755 /// ditto
756 auto rotorIIIG312(dchar ringSetting = 'A') pure
757 {
758     return rotor("UQNTLSZFMREHDPXKIBVYGJCWOA", 'U', 'W', 'X', 'A', 'E', 'F', 'H', 'K', 'M', 'N', 'R', ringSetting);
759 }
760 
761 /// ditto
762 auto rotorIG111(dchar ringSetting = 'A') pure
763 {
764     return rotor("WLRHBQUNDKJCZSEXOTMAGYFPVI", 'S', 'U', 'V', 'W', 'Z', 'A', 'B', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'O', 'P', 'Q', ringSetting);
765 }
766 
767 /// ditto
768 auto rotorIIG111(dchar ringSetting = 'A') pure
769 {
770     return rotor("TFJQAZWMHLCUIXRDYGOEVBNSKP", 'S', 'T', 'V', 'Y', 'Z', 'A', 'C', 'D', 'F', 'G', 'H', 'K', 'M', 'N', 'Q', ringSetting);
771 }
772 
773 /// ditto
774 auto rotorVG111(dchar ringSetting = 'A') pure
775 {
776     return rotor("QTPIXWVDFRMUSLJOHCANEZKYBG", 'S', 'W', 'Z', 'F', 'H', 'M', 'Q', ringSetting);
777 }
778 
779 /++
780 + Predefined existent rotors. Because these rotors have no turnover notches, they are generally set
781 + side by side with a reflector.
782 +/
783 auto rotorBeta(dchar ringSetting = 'A') pure
784 {
785     return rotor("LEYJVCNIXWPBQMDRTAKZGFUHOS", ringSetting);
786 }
787 
788 /// ditto
789 auto rotorGamma(dchar ringSetting = 'A') pure
790 {
791     return rotor("FSOKANUERHMBTIYCWLQPZXVGJD", ringSetting);
792 }
793 
794 /// Predefined the simplest entry wheel which does not substitute.
795 auto entryWheelABC() pure
796 {
797     return entryWheel("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
798 }
799 
800 /// Predefined entry wheel: QWE... -> ABC...
801 auto entryWheelQWE() pure
802 {
803     return entryWheel("QWERTZUIOASDFGHJKPYXCVBNML");
804 }
805 
806 /// Predefined entry wheel: KZR... -> ABC...
807 auto entryWheelKZR() pure
808 {
809     return entryWheel("KZROUQHYAIGBLWVSTDXFPNMCJE");
810 }
811 
812 /// Predefined the simplest plugboard which does not substitute.
813 auto plugboardDoNothing() pure
814 {
815     return plugboard("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
816 }
817 
818 /// Predefined existent reflectors.
819 auto reflectorA() pure
820 {
821     return reflector("EJMZALYXVBWFCRQUONTSPIKHGD");
822 }
823 
824 auto reflectorB() pure
825 {
826     return reflector("YRUHQSLDPXNGOKMIEBFZCWVJAT");
827 }
828 
829 /// ditto
830 auto reflectorC() pure
831 {
832     return reflector("FVPJIAOYEDRZXWGCTKUQSBNMHL");
833 }
834 
835 /// ditto
836 auto reflectorBThin() pure
837 {
838     return reflector("ENKQAUYWJICOPBLMDXZVFTHRGS");
839 }
840 
841 /// ditto
842 auto reflectorCThin() pure
843 {
844     return reflector("RDOBJNTKVEHMLFCWZAXGYIPSUQ");
845 }
846 
847 /// ditto
848 auto reflectorNor() pure
849 {
850     return reflector("MOWJYPUXNDSRAIBFVLKZGQCHET");
851 }
852 
853 /// ditto
854 auto reflectorD(dchar ringSetting = 'A') pure
855 {
856     return reflector("IMETCGFRAYSQBZXWLHKDVUPOJN", ringSetting);
857 }
858 
859 /// ditto
860 alias reflectorK = reflectorD;
861 
862 /// ditto
863 auto reflectorR(dchar ringSetting = 'A') pure
864 {
865     return reflector("QYHOGNECVPUZTFDJAXWMKISRBL", ringSetting);
866 }
867 
868 /// ditto
869 auto reflectorT(dchar ringSetting = 'A') pure
870 {
871     return reflector("GEKPBTAUMOCNILJDXZYFHWVQSR", ringSetting);
872 }
873 
874 /// ditto
875 auto reflectorFRA(dchar ringSetting = 'A') pure
876 {
877     return reflector("KOTVPNLMJIAGHFBEWYXCZDQSRU", ringSetting);
878 }
879 
880 /// ditto
881 alias reflectorA865 = reflectorD;
882 
883 /// ditto
884 alias reflectorG260 = reflectorD;
885 
886 /// ditto
887 auto reflectorG312(dchar ringSetting = 'A') pure
888 {
889     return reflector("RULQMZJSYGOCETKWDAHNBXPVIF", ringSetting);
890 }
891 
892 /// ditto
893 alias reflectorG111 = reflectorD;
894 
895 // Double stepping test (http://www.cryptomuseum.com/crypto/enigma/working.htm)
896 unittest
897 {
898     auto m3 = EnigmaM3(plugboardDoNothing, entryWheelABC, rotorI, rotorII, rotorIII, reflectorB, "ODA");
899 
900     assert(m3.rotationStates == [14, 3, 0, 0]);
901 
902     assert(m3('A') == 'H');
903     assert(m3.rotationStates == [15, 3, 0, 0]);
904 
905     assert(m3('A') == 'D');
906     assert(m3.rotationStates == [16, 3, 0, 0]);
907 
908     assert(m3('A') == 'Z');
909     assert(m3.rotationStates == [17, 4, 0, 0]);
910 
911     assert(m3('A') == 'G');
912     assert(m3.rotationStates == [18, 5, 1, 0]);
913 
914     assert(m3('A') == 'O');
915     assert(m3.rotationStates == [19, 5, 1, 0]);
916 
917     assert(m3('A') == 'V');
918     assert(m3.rotationStates == [20, 5, 1, 0]);
919 }
920 
921 // The noches are fixed to the ring.
922 unittest
923 {
924     auto ed = EnigmaD(entryWheelQWE, rotorID, rotorIID, rotorIIID, reflectorD, "UDN" /*!*/ , 'B');
925 
926     assert(ed.rotationStates == [20, 3, 13, 1]);
927 
928     assert(ed('A') == 'Z');
929     assert(ed.rotationStates == [21, 3, 13, 1]);
930 
931     assert(ed('A') == 'D');
932     assert(ed.rotationStates == [22, 3, 13, 1]);
933 
934     assert(ed('A') == 'V');
935     assert(ed.rotationStates == [23, 3, 13, 1]);
936 
937     assert(ed('A') == 'I');
938     assert(ed.rotationStates == [24, 3, 13, 1]);
939 
940     assert(ed('A') == 'C');
941     assert(ed.rotationStates == [25, 4, 13, 1]);
942 
943     assert(ed('A') == 'Z');
944     assert(ed.rotationStates == [0, 5, 14, 1]);
945 
946 
947     // The K's rotor positions are same as the D's.
948     auto sk = SwissK(entryWheelQWE, rotorIK('Z'), rotorIIK('Y'), rotorIIIK, reflectorK('E'), "UDN" /*!*/ , 'X');
949 
950     assert(sk.rotationStates == [20, 3, 13, 23]);
951 
952     assert(sk('A') == 'Y');
953     assert(sk.rotationStates == [21, 3, 13, 23]);
954 
955     assert(sk('A') == 'H');
956     assert(sk.rotationStates == [22, 3, 13, 23]);
957 
958     assert(sk('A') == 'U');
959     assert(sk.rotationStates == [23, 3, 13, 23]);
960 
961     assert(sk('A') == 'M');
962     assert(sk.rotationStates == [24, 3, 13, 23]);
963 
964     assert(sk('A') == 'V');
965     assert(sk.rotationStates == [25, 4, 13, 23]);
966 
967     assert(sk('A') == 'Q');
968     assert(sk.rotationStates == [0, 5, 14, 23]);
969 }
970 
971 unittest
972 {
973     auto nor1 = Norway(entryWheelABC, rotorINor, rotorIINor, rotorIIINor, reflectorNor, "PDL");
974 
975     assert(nor1('A') == 'I');
976     assert(nor1('A') == 'F');
977     assert(nor1('A') == 'L');
978     assert(nor1('A') == 'F');
979     assert(nor1('A') == 'F');
980     assert(nor1('A') == 'H');
981 
982 
983     auto nor2 = Norway(plugboard("ABCDEGFHIJKLMNOPQRSTUVWXYZ"), entryWheelABC, rotorINor, rotorIVNor, rotorVNor, reflectorNor, "PDL");
984 
985     assert(nor2('A') == 'P');
986     assert(nor2('A') == 'G');
987     assert(nor2('A') == 'J');
988     assert(nor2('A') == 'N');
989     assert(nor2('A') == 'U');
990     assert(nor2('A') == 'Z');
991 }
992 
993 unittest
994 {
995     auto er = EnigmaR(entryWheelQWE, rotorIR, rotorIIR, rotorIIIR, reflectorR('E'), "KBA", 'C');
996 
997     assert(er('A') == 'Y');
998     assert(er('A') == 'P');
999     assert(er('A') == 'I');
1000     assert(er('A') == 'R');
1001     assert(er('A') == 'Z');
1002     assert(er('A') == 'S');
1003 }
1004 
1005 unittest
1006 {
1007     auto et1 = EnigmaT(entryWheelKZR, rotorIT, rotorIIT, rotorIIIT, reflectorT, "AFR", 'H');
1008 
1009     assert(et1('A') == 'E');
1010     assert(et1('A') == 'T');
1011     assert(et1('A') == 'U');
1012     assert(et1('A') == 'I');
1013     assert(et1('A') == 'H');
1014     assert(et1('A') == 'M');
1015 
1016 
1017     auto et2 = EnigmaT(entryWheelKZR, rotorVIT, rotorVT, rotorIVT, reflectorT, "AFR", 'A');
1018 
1019     assert(et2('A') == 'X');
1020     assert(et2('A') == 'F');
1021     assert(et2('A') == 'M');
1022     assert(et2('A') == 'R');
1023     assert(et2('A') == 'C');
1024     assert(et2('A') == 'B');
1025 
1026 
1027     auto et3 = EnigmaT(entryWheelKZR, rotorVIIT, rotorVIIIT('Z'), rotorIVT, reflectorT('C'), "AFR", 'V');
1028 
1029     assert(et3('A') == 'D');
1030     assert(et3('A') == 'S');
1031     assert(et3('A') == 'F');
1032     assert(et3('A') == 'N');
1033     assert(et3('A') == 'D');
1034     assert(et3('A') == 'T');
1035 }
1036 
1037 unittest
1038 {
1039     auto ekd = EnigmaKD(entryWheelQWE, rotorIKD, rotorIIKD, rotorIIIKD, reflectorFRA, "AAA");
1040 
1041     assert(ekd('A') == 'W');
1042     assert(ekd('A') == 'C');
1043     assert(ekd('A') == 'H');
1044     assert(ekd('A') == 'U');
1045     assert(ekd('A') == 'G');
1046     assert(ekd('A') == 'K');
1047 }
1048 
1049 // Normal stepping and movable reflector test
1050 unittest
1051 {
1052     auto ea865 = EnigmaA28(entryWheelQWE, rotorIIIA865, rotorIIA865, rotorIA865, reflectorA865, "CAA", 'A');
1053 
1054     assert(ea865.rotationStates == [2, 0, 0, 0]);
1055 
1056     assert(ea865('A') == 'Q');
1057     assert(ea865.rotationStates == [3, 0, 0, 0]);
1058 
1059     assert(ea865('A') == 'I');
1060     assert(ea865.rotationStates == [4, 0, 0, 0]);
1061 
1062     assert(ea865('A') == 'C');
1063     assert(ea865.rotationStates == [5, 1, 1, 1]);
1064 
1065     assert(ea865('A') == 'Y');
1066     assert(ea865.rotationStates == [6, 2, 1, 1]);
1067 
1068     assert(ea865('A') == 'D');
1069     assert(ea865.rotationStates == [7, 2, 1, 1]);
1070 
1071     assert(ea865('A') == 'E');
1072     assert(ea865.rotationStates == [8, 3, 2, 2]);
1073 
1074     assert(ea865('A') == 'D');
1075     assert(ea865.rotationStates == [9, 3, 2, 2]);
1076 
1077     assert(ea865('A') == 'Z');
1078     assert(ea865.rotationStates == [10, 3, 2, 2]);
1079 
1080     assert(ea865('A') == 'X');
1081     assert(ea865.rotationStates == [11, 4, 3, 3]);
1082 }
1083 
1084 unittest
1085 {
1086     auto eg260 = EnigmaG(entryWheelQWE, rotorIG260, rotorIIG260, rotorIIIG260, reflectorG260, "AAA", 'A');
1087 
1088     assert(eg260.rotationStates == [0, 0, 0, 0]);
1089 
1090     assert(eg260('A') == 'C');
1091     assert(eg260.rotationStates == [1, 1, 1, 1]);
1092 
1093     assert(eg260('A') == 'K');
1094     assert(eg260.rotationStates == [2, 2, 1, 1]);
1095 
1096     assert(eg260('A') == 'N');
1097     assert(eg260.rotationStates == [3, 3, 2, 1]);
1098 
1099     assert(eg260('A') == 'U');
1100     assert(eg260.rotationStates == [4, 3, 2, 1]);
1101 
1102     assert(eg260('A') == 'L');
1103     assert(eg260.rotationStates == [5, 4, 3, 1]);
1104 
1105     assert(eg260('A') == 'Q');
1106     assert(eg260.rotationStates == [6, 5, 3, 1]);
1107 
1108     assert(eg260('A') == 'Y');
1109     assert(eg260.rotationStates == [7, 6, 4, 1]);
1110 
1111     assert(eg260('A') == 'P');
1112     assert(eg260.rotationStates == [8, 6, 4, 1]);
1113 
1114     assert(eg260('A') == 'I');
1115     assert(eg260.rotationStates == [9, 7, 5, 2]);
1116 }
1117 
1118 unittest
1119 {
1120     auto eg312 = EnigmaG(entryWheelQWE, rotorIG312, rotorIIG312('B'), rotorIIIG312, reflectorG312('Y'), "CZB", 'D');
1121 
1122     assert(eg312.rotationStates == [2, 25, 1, 3]);
1123 
1124     assert(eg312('A') == 'J');
1125     assert(eg312.rotationStates == [3, 0, 2, 3]);
1126 
1127     assert(eg312('A') == 'B');
1128     assert(eg312.rotationStates == [4, 0, 2, 3]);
1129 
1130     assert(eg312('A') == 'X');
1131     assert(eg312.rotationStates == [5, 1, 3, 3]);
1132 
1133     assert(eg312('A') == 'K');
1134     assert(eg312.rotationStates == [6, 2, 3, 3]);
1135 
1136     assert(eg312('A') == 'N');
1137     assert(eg312.rotationStates == [7, 3, 4, 3]);
1138 
1139     assert(eg312('A') == 'N');
1140     assert(eg312.rotationStates == [8, 3, 4, 3]);
1141 
1142     assert(eg312('A') == 'C');
1143     assert(eg312.rotationStates == [9, 4, 5, 4]);
1144 }
1145 
1146 unittest
1147 {
1148     auto eg111 = EnigmaG(entryWheelQWE, rotorIG111, rotorVG111, rotorIIG111, reflectorG111, "AAA", 'A');
1149 
1150     assert(eg111.rotationStates == [0, 0, 0, 0]);
1151 
1152     assert(eg111('A') == 'E');
1153     assert(eg111.rotationStates == [1, 1, 0, 0]);
1154 
1155     assert(eg111('A') == 'L');
1156     assert(eg111.rotationStates == [2, 2, 0, 0]);
1157 
1158     assert(eg111('A') == 'U');
1159     assert(eg111.rotationStates == [3, 3, 0, 0]);
1160 
1161     assert(eg111('A') == 'X');
1162     assert(eg111.rotationStates == [4, 3, 0, 0]);
1163 
1164     assert(eg111('A') == 'L');
1165     assert(eg111.rotationStates == [5, 4, 0, 0]);
1166 
1167     assert(eg111('A') == 'U');
1168     assert(eg111.rotationStates == [6, 5, 0, 0]);
1169 
1170     assert(eg111('A') == 'P');
1171     assert(eg111.rotationStates == [7, 6, 1, 1]);
1172 
1173     assert(eg111('A') == 'E');
1174     assert(eg111.rotationStates == [8, 6, 1, 1]);
1175 
1176     assert(eg111('A') == 'Q');
1177     assert(eg111.rotationStates == [9, 7, 1, 1]);
1178 }
1179 
1180 /// Step-by-step enciphering.
1181 unittest
1182 {
1183     immutable pbCI = plugboard("ABIDEFGHCJKLMNOPQRSTUVWXYZ"); // C <-> I
1184     immutable enWh = entryWheel("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
1185     immutable refB = reflector("YRUHQSLDPXNGOKMIEBFZCWVJAT");
1186     immutable rot1 = rotor("EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'Q', 'A');
1187     immutable rot2 = rotor("AJDKSIRUXBLHWTMCQGZNPYFVOE", 'E', 'B');
1188     immutable rot3 = rotor("BDFHJLCPRTXVZNYEIWGAKMUSQO", 'V', 'A');
1189 
1190     auto e3 = Enigma!(3, EnigmaType.plugboard)(pbCI, enWh, rot1, rot2, rot3, refB, ['X', 'Q', 'E']);
1191     assert(e3('A') == 'K');
1192     assert(e3('a') == 'T'); // A lowercase is automatically converted to an uppercase.
1193     assert(e3('5') == '5'); // A non-alphabetical character does not changes
1194     assert(e3('Ü') == 'Ü'); // the machine state and will be output as it is.
1195     assert(e3('A') == 'Q');
1196 }
1197 
1198 /// Encipherment is decipherment.
1199 unittest
1200 {
1201     // These have the same settings.
1202     auto encipherer = Enigma!2(entryWheelABC, rotorVI, rotorVII, reflectorC, "PY");
1203     auto decipherer = Enigma!2(entryWheelABC, rotorVI, rotorVII, reflectorC, "PY");
1204 
1205     foreach (dchar c; "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1206     {
1207         auto enciphered = encipherer(c);
1208         auto deciphered = decipherer(enciphered);
1209     }
1210 }
1211 
1212 /++
1213  + A certain equivalence of the M3 and the M4.
1214  +/
1215 unittest
1216 {
1217     // These have the equivalent settings.
1218     auto m3 = EnigmaM3(entryWheelABC, rotorIII, rotorII, rotorI, reflectorB /*!*/ ,
1219         "FOO");
1220     auto m4 = EnigmaM4(entryWheelABC, rotorIII, rotorII, rotorI,
1221         rotorBeta('A') /*!*/ , reflectorBThin /*!*/ , "FOOA" /*!*/ ); // FOO*A*
1222 
1223     // If each machine has just one movable rotor...
1224     auto e1 = Enigma!1(entryWheelABC, rotorI, reflectorC /*!*/ , "X");
1225     auto e2fixed = Enigma!(2, EnigmaType.fixedFinalRotor  /*!*/ )(entryWheelABC, rotorI,
1226         rotorGamma('A') /*!*/ , reflectorCThin /*!*/ , "XA" /*!*/ ); // X*A*
1227 
1228     foreach (dchar c; "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1229     {
1230         assert(m3(c) == m4(c));
1231         assert(e1(c) == e2fixed(c));
1232     }
1233 }
1234 
1235 /// Encipher with the M4 and decipher with the equivalent M3.
1236 unittest
1237 {
1238     import std.algorithm.comparison : equal;
1239     import std.algorithm.iteration : each, map;
1240     import std.array : appender;
1241 
1242     // These have the equivalent settings.
1243     auto m4 = EnigmaM4(plugboard("SBCDEGFHIJKLMNOPQRATUVWXYZ"), entryWheelABC, rotorIII('Y'),
1244         rotorII('V'), rotorI('R'), rotorBeta, reflectorBThin, "UEQA");
1245     auto m3 = EnigmaM3(plugboard("SBCDEGFHIJKLMNOPQRATUVWXYZ"), entryWheelABC, rotorIII('Y'),
1246         rotorII('V'), rotorI('R'), reflectorB, "UEQ");
1247 
1248     auto enciphered = appender!dstring;
1249     "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map!m4.each!(c => enciphered.put(c));
1250     assert(enciphered.data == "RIIGSIBEBIZKCTZSSDGQMLSVUX");
1251 
1252     auto deciphered = enciphered.data.map!m3;
1253     assert(deciphered.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
1254 }