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