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     /++
34      + Constructs a rotor with no turnover notches.
35      + If ringOffset is `2`, it corresponds to "C-03".
36      +/
37     this()(in auto ref BSM!N perm, size_t ringOffset) pure
38     {
39         import boolean_matrix : lowerRotator, upperRotator;
40 
41         this.perm = lowerRotator!N(ringOffset) * perm * upperRotator!N(ringOffset);
42     }
43 
44     /++
45      + Constructs a rotor with one turnover notch.
46      + If turnover is `1`, the next rotor steps when this rotor steps from B to C.
47      + If ringOffset is `2`, it corresponds to "C-03".
48      +/
49     this()(in auto ref BSM!N perm, size_t turnover, size_t ringOffset) pure
50     {
51         this(perm, ringOffset);
52         this.turnovers = [turnover % N];
53         hasNotch = true;
54     }
55 
56     /++
57      + Constructs a rotor with two turnover notches.
58      + If turnover1 is `1` and turnover2 is `25`, the next rotor steps
59      + when this rotor steps from B to C and from Z to A.
60      + If ringOffset is `2`, it corresponds to "C-03".
61      +/
62     this()(in auto ref BSM!N perm, size_t turnover1, size_t turnover2, size_t ringOffset) pure
63     {
64         import std.algorithm.sorting : sort;
65         import std.array : array;
66 
67         this(perm, ringOffset);
68         this.turnovers = [turnover1 % N, turnover2 % N].sort().array;
69         hasNotch = true;
70     }
71 }
72 
73 /++
74  + A convenience function to make a rotor with no turnover notches
75  + from a forward substitution.
76  + If ringSetting is `'C'`, it corresponds to "C-03".
77  +/
78 auto rotor(S)(in S forwardSubstitution, dchar ringSetting) pure if (isSomeStringOrDcharRange!S)
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     assert(ringSetting.isAlpha);
89 }
90 body
91 {
92     import std.algorithm.iteration : map;
93     import std.array : array;
94     import std.ascii : toUpper;
95     import boolean_matrix : permutation;
96 
97     return Rotor(forwardSubstitution.map!toUpper.map!"a-'A'".array.permutation!N,
98         ringSetting.toUpper - 'A');
99 }
100 
101 /++
102  + A convenience function to make a rotor with one turnover notch
103  + from a forward substitution.
104  + If turnover is `'B'`, the next rotor steps when this rotor steps from B to C.
105  + If ringSetting is `'C'`, it corresponds to "C-03".
106  +/
107 auto rotor(S)(in S forwardSubstitution, dchar turnover, dchar ringSetting) pure if (isSomeStringOrDcharRange!S)
108 in
109 {
110     import std.algorithm.comparison : isPermutation;
111     import std.algorithm.iteration : map;
112     import std.ascii : isAlpha, toUpper;
113     import std.range : iota, walkLength;
114 
115     assert(forwardSubstitution.walkLength == N, "Bad length.");
116     assert(N.iota.isPermutation(forwardSubstitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
117     assert(turnover.isAlpha);
118     assert(ringSetting.isAlpha);
119 }
120 body
121 {
122     import std.algorithm.iteration : map;
123     import std.array : array;
124     import std.ascii : toUpper;
125     import boolean_matrix : permutation;
126 
127     return Rotor(forwardSubstitution.map!toUpper.map!"a-'A'".array.permutation!N,
128         turnover.toUpper - 'A', ringSetting.toUpper - 'A');
129 }
130 
131 /++
132  + A convenience function to make a rotor with two turnover notches
133  + from a forward substitution.
134  + If turnover1 is `'B'` and turnover2 is `'Z'`, the next rotor steps
135  + when this rotor steps from B to C and from Z to A.
136 + If ringSetting is `'C'`, it corresponds to "C-03".
137  +/
138 auto rotor(S)(in S forwardSubstitution, dchar turnover1, dchar turnover2, dchar ringSetting) pure if (isSomeStringOrDcharRange!S)
139 in
140 {
141     import std.algorithm.comparison : isPermutation;
142     import std.algorithm.iteration : map;
143     import std.ascii : isAlpha, toUpper;
144     import std.range : iota, walkLength;
145 
146     assert(forwardSubstitution.walkLength == N, "Bad length.");
147     assert(N.iota.isPermutation(forwardSubstitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
148     assert(turnover1.isAlpha);
149     assert(turnover2.isAlpha);
150     assert(ringSetting.isAlpha);
151 }
152 body
153 {
154     import std.algorithm.iteration : map;
155     import std.array : array;
156     import std.ascii : toUpper;
157     import boolean_matrix : permutation;
158 
159     return Rotor(forwardSubstitution.map!toUpper.map!"a-'A'".array.permutation!N,
160         turnover1.toUpper - 'A', turnover2.toUpper - 'A', ringSetting.toUpper - 'A');
161 }
162 
163 ///
164 struct EntryWheel
165 {
166     import boolean_matrix : BSM;
167 
168     immutable BSM!N perm;
169     ///
170     this()(in auto ref BSM!N perm) pure
171     {
172         this.perm = cast(immutable) perm;
173     }
174 
175     alias perm this;
176 }
177 
178 /++
179  + A convenience function to make an entry wheel from a substitution.
180  +/
181 auto entryWheel(S)(in S backwardSubstitution) pure if (isSomeStringOrDcharRange!S)
182 in
183 {
184     import std.algorithm.comparison : isPermutation;
185     import std.algorithm.iteration : map;
186     import std.ascii : toUpper;
187     import std.range : iota, walkLength;
188 
189     assert(backwardSubstitution.walkLength == N, "Bad length.");
190     assert(N.iota.isPermutation(backwardSubstitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
191 }
192 body
193 {
194     import std.algorithm.iteration : map;
195     import std.array : array;
196     import std.ascii : toUpper;
197     import boolean_matrix : permutation, transpose;
198 
199     return EntryWheel(backwardSubstitution.map!toUpper.map!"a-'A'".array.permutation!N.transpose);
200 }
201 
202 ///
203 struct Plugboard
204 {
205     import boolean_matrix : BSM;
206 
207     immutable BSM!N perm;
208     ///
209     this()(in auto ref BSM!N perm) pure
210     {
211         this.perm = cast(immutable) perm;
212     }
213 
214     alias perm this;
215 }
216 
217 /++
218  + A convenience function to make a plugboard from a substitution.
219  +/
220 auto plugboard(S)(in S substitution) pure if (isSomeStringOrDcharRange!S)
221 in
222 {
223     import std.algorithm.comparison : isPermutation;
224     import std.algorithm.iteration : map;
225     import std.ascii : toUpper;
226     import std.range : iota, walkLength;
227 
228     assert(substitution.walkLength == N, "Bad length.");
229     assert(N.iota.isPermutation(substitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
230 }
231 out (r)
232 {
233     import boolean_matrix : transpose;
234 
235     assert(r == r.transpose);
236 }
237 body
238 {
239     import std.algorithm.iteration : map;
240     import std.array : array;
241     import std.ascii : toUpper;
242     import boolean_matrix : permutation;
243 
244     return Plugboard(substitution.map!toUpper.map!"a-'A'".array.permutation!N);
245 }
246 
247 ///
248 struct Reflector
249 {
250     import boolean_matrix : BSM;
251 
252     immutable BSM!N perm;
253     ///
254     this()(in auto ref BSM!N perm, size_t ringOffset) pure
255     {
256         import boolean_matrix : lowerRotator, upperRotator;
257 
258         this.perm = lowerRotator!N(ringOffset) * perm * upperRotator!N(ringOffset);
259     }
260 
261     alias perm this;
262 }
263 
264 /++
265  + A convenience function to make a reflector from a substitution.
266  +/
267 auto reflector(S)(in S substitution, dchar ringSetting = 'A') pure if (isSomeStringOrDcharRange!S)
268 in
269 {
270     import std.algorithm.comparison : isPermutation;
271     import std.algorithm.iteration : map;
272     import std.ascii : isAlpha, toUpper;
273     import std.range : iota, walkLength;
274 
275     assert(substitution.walkLength == N, "Bad length.");
276     assert(N.iota.isPermutation(substitution.map!toUpper.map!"a-'A'"), "Bad permutation.");
277     assert(ringSetting.isAlpha);
278 }
279 out (r)
280 {
281     import boolean_matrix : transpose;
282 
283     assert(r == r.transpose);
284 }
285 body
286 {
287     import std.algorithm.iteration : map;
288     import std.array : array;
289     import std.ascii : toUpper;
290     import boolean_matrix : permutation;
291 
292     return Reflector(substitution.map!toUpper.map!"a-'A'".array.permutation!N,
293         ringSetting.toUpper - 'A');
294 }
295 
296 /// Currently machines with the double-stepping mechanism are available.
297 struct Enigma(size_t rotorN, bool fixedFinalRotor = false, bool hasPlugboard = true, bool settableReflectorPos = false)
298 {
299     import boolean_matrix : BSM;
300     private immutable BSM!N composedInputPerm;
301     private immutable Rotor[rotorN] rotors;
302     private immutable BSM!N reflector;
303     private size_t[rotorN] rotationStates;
304 
305     import meta_workaround : Repeat;
306 
307     ///
308     this(in EntryWheel entryWheel, in Repeat!(rotorN, Rotor) rotors,
309         in Reflector reflector, in dchar[rotorN] rotorStartPos)
310     in
311     {
312         foreach (dchar c; rotorStartPos)
313         {
314             import std.ascii : isAlpha;
315 
316             assert(c.isAlpha);
317         }
318     }
319     body
320     {
321         foreach (i, ref e; rotationStates)
322         {
323             import std.ascii : toUpper;
324 
325             e = rotorStartPos[i].toUpper - 'A';
326         }
327 
328         this.composedInputPerm = cast(immutable) entryWheel.perm;
329         this.rotors[] = cast(immutable)[rotors][];
330         this.reflector = cast(immutable) reflector.perm;
331     }
332 
333     ///
334     static if (settableReflectorPos)
335     {
336         this(in EntryWheel entryWheel, in Repeat!(rotorN, Rotor) rotors,
337             in Reflector reflector, in dchar[rotorN] rotorStartPos,
338             dchar reflectorPos)
339         in
340         {
341             import std.ascii : isAlpha;
342 
343             assert(reflectorPos.isAlpha);
344         }
345         body
346         {
347             this(entryWheel, rotors, reflector, rotorStartPos);
348 
349             import std.ascii : toUpper;
350             import boolean_matrix : lowerRotator, upperRotator;
351 
352             immutable refOffset = reflectorPos.toUpper - 'A';
353             this.reflector = upperRotator!N(refOffset) * reflector * lowerRotator!N(refOffset);
354         }
355     }
356 
357     ///
358     static if (hasPlugboard)
359     {
360         this(in Plugboard plugboard, in EntryWheel entryWheel,
361             in Repeat!(rotorN, Rotor) rotors,
362             in Reflector reflector, in dchar[rotorN] rotorStartPos)
363         {
364             this(entryWheel, rotors, reflector, rotorStartPos);
365             this.composedInputPerm = cast(immutable) (entryWheel * plugboard);
366         }
367 
368         ///
369         static if (settableReflectorPos)
370         {
371             this(in Plugboard plugboard, in EntryWheel entryWheel,
372                 in Repeat!(rotorN, Rotor) rotors,
373                 in Reflector reflector, in dchar[rotorN] rotorStartPos,
374                 dchar reflectorPos)
375             in
376             {
377                 import std.ascii : isAlpha;
378 
379                 assert(reflectorPos.isAlpha);
380             }
381             body
382             {
383                 this(plugboard, entryWheel, rotors, reflector, rotorStartPos);
384 
385                 import std.ascii : toUpper;
386                 import boolean_matrix : lowerRotator, upperRotator;
387 
388                 immutable refOffset = reflectorPos.toUpper - 'A';
389                 this.reflector = upperRotator!N(refOffset) * reflector * lowerRotator!N(refOffset);
390             }
391         }
392     }
393 
394     private void step()
395     {
396         enum movableRotorN = fixedFinalRotor ? rotorN - 1 : rotorN;
397         bool[movableRotorN] stepFlag;
398 
399         stepFlag[0] = true;
400 
401         // Handles double stepping
402         foreach (rotorID; 0 .. movableRotorN - 1)
403         {
404             import std.algorithm.searching : canFind;
405 
406             if (rotors[rotorID].turnovers.canFind(rotationStates[rotorID]))
407             {
408                 stepFlag[rotorID] = true;
409                 stepFlag[rotorID + 1] = true;
410             }
411         }
412 
413         foreach (rotorID, e; stepFlag)
414         {
415             if (e)
416             {
417                 rotationStates[rotorID] = (rotationStates[rotorID] + 1) % N;
418             }
419         }
420     }
421 
422     import boolean_matrix : BSM;
423 
424     /+
425      + fwdPerm = (Um*Rm*Lm)*(Um-1*Rm-1*Lm-1)*...*(U0*R0*L0)*P
426      +         = Um*(Rm*Lm*Um-1)*(Rm-1*Lm-1*Um-2)*...*(R0*L0)*P
427      +         = Um*(Rm*RELm)*(Rm-1*RELm-1)*...*(R0*L0)*P
428      +/
429     private BSM!N composeForwardPermutation(in ref BSM!N prevPerm, size_t rotorID)
430     {
431         import boolean_matrix : lowerRotator, upperRotator, Identity;
432 
433         immutable ptrdiff_t x = rotorID == 0 ? rotationStates[0] : rotationStates[rotorID] - rotationStates[rotorID - 1];
434         immutable relRotator = x > 0 ? lowerRotator!N(x) : upperRotator!N(-x);
435         immutable composedPerm = rotors[rotorID].perm * relRotator * prevPerm;
436         return rotorID == rotorN - 1 ? upperRotator!N(rotationStates[rotorID]) * composedPerm
437             : composeForwardPermutation(composedPerm, rotorID + 1);
438     }
439 
440     private auto process(size_t keyInputID)
441     {
442         step();
443 
444         import boolean_matrix : transpose, BCV;
445 
446         immutable fwdPerm = composeForwardPermutation(composedInputPerm, 0);
447         // bwdPerm = fwdPerm^-1 = fwdPerm^T
448         immutable wholePerm = fwdPerm.transpose * reflector * fwdPerm;
449         BCV!N v;
450         v[keyInputID] = true;
451         immutable w = wholePerm * v;
452         import std.algorithm.searching : countUntil;
453 
454         immutable r = w[].countUntil(true);
455         assert(r >= 0);
456         return r;
457 
458     }
459 
460     /// Enciphers only an alphabetical character through the current Enigma machine.
461     dchar opCall(dchar keyInput)
462     {
463         import std.ascii : isAlpha, toUpper;
464 
465         return keyInput.isAlpha ? process(keyInput.toUpper - 'A') + 'A' : keyInput;
466     }
467 }
468 
469 /// Enigma I 'Wehrmacht', which has three rotor slots.
470 alias EnigmaI = Enigma!3;
471 
472 /// Enigma M3, which has three rotor slots.
473 alias EnigmaM3 = Enigma!3;
474 
475 /// Enigma M4, which has four rotor slots. The fourth rotor never rotates.
476 alias EnigmaM4 = Enigma!(4, true);
477 
478 /// Enigma D, which has three rotor slots and no plugboard. The reflector can be set to any positions.
479 alias EnigmaD = Enigma!(3, false, false, true);
480 
481 /// Swiss K, which has three rotor slots and no plugboard. The reflector can be set to any positions.
482 alias SwissK = EnigmaD;
483 
484 /// Predefined existent rotors.
485 auto rotorI(dchar ringSetting = 'A') pure
486 {
487     return rotor("EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'Q', ringSetting);
488 }
489 
490 /// ditto
491 auto rotorII(dchar ringSetting = 'A') pure
492 {
493     return rotor("AJDKSIRUXBLHWTMCQGZNPYFVOE", 'E', ringSetting);
494 }
495 
496 /// ditto
497 auto rotorIII(dchar ringSetting = 'A') pure
498 {
499     return rotor("BDFHJLCPRTXVZNYEIWGAKMUSQO", 'V', ringSetting);
500 }
501 
502 /// ditto
503 auto rotorIV(dchar ringSetting = 'A') pure
504 {
505     return rotor("ESOVPZJAYQUIRHXLNFTGKDCMWB", 'J', ringSetting);
506 }
507 
508 /// ditto
509 auto rotorV(dchar ringSetting = 'A') pure
510 {
511     return rotor("VZBRGITYUPSDNHLXAWMJQOFECK", 'Z', ringSetting);
512 }
513 
514 /// ditto
515 auto rotorVI(dchar ringSetting = 'A') pure
516 {
517     return rotor("JPGVOUMFYQBENHZRDKASXLICTW", 'Z', 'M', ringSetting);
518 }
519 
520 /// ditto
521 auto rotorVII(dchar ringSetting = 'A') pure
522 {
523     return rotor("NZJHGRCXMYSWBOUFAIVLPEKQDT", 'Z', 'M', ringSetting);
524 }
525 
526 /// ditto
527 auto rotorVIII(dchar ringSetting = 'A') pure
528 {
529     return rotor("FKQHTLXOCBJSPDZRAMEWNIUYGV", 'Z', 'M', ringSetting);
530 }
531 
532 /// ditto
533 auto rotorID(dchar ringSetting = 'A') pure
534 {
535     return rotor("LPGSZMHAEOQKVXRFYBUTNICJDW", 'Y', ringSetting);
536 }
537 
538 /// ditto
539 auto rotorIID(dchar ringSetting = 'A') pure
540 {
541     return rotor("SLVGBTFXJQOHEWIRZYAMKPCNDU", 'E', ringSetting);
542 }
543 
544 /// ditto
545 auto rotorIIID(dchar ringSetting = 'A') pure
546 {
547     return rotor("CJGDPSHKTURAWZXFMYNQOBVLIE", 'N', ringSetting);
548 }
549 
550 /// ditto
551 auto rotorIK(dchar ringSetting = 'A') pure
552 {
553     return rotor("PEZUOHXSCVFMTBGLRINQJWAYDK", 'Y', ringSetting);
554 }
555 
556 /// ditto
557 auto rotorIIK(dchar ringSetting = 'A') pure
558 {
559     return rotor("ZOUESYDKFWPCIQXHMVBLGNJRAT", 'E', ringSetting);
560 }
561 
562 /// ditto
563 auto rotorIIIK(dchar ringSetting = 'A') pure
564 {
565     return rotor("EHRVXGAOBQUSIMZFLYNWKTPDJC", 'N', ringSetting);
566 }
567 
568 /++
569 + Predefined existent rotors. Because these rotors have no turnover notches, they are generally set
570 + side by side with a reflector.
571 +/
572 auto rotorBeta(dchar ringSetting = 'A') pure
573 {
574     return rotor("LEYJVCNIXWPBQMDRTAKZGFUHOS", ringSetting);
575 }
576 
577 /// ditto
578 auto rotorGamma(dchar ringSetting = 'A') pure
579 {
580     return rotor("FSOKANUERHMBTIYCWLQPZXVGJD", ringSetting);
581 }
582 
583 /// Predefined the simplest entry wheel which does not substitute.
584 auto entryWheelABC() pure
585 {
586     return entryWheel("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
587 }
588 
589 /// Predefined entry wheel: QWE... -> ABC...
590 auto entryWheelQWE() pure
591 {
592     return entryWheel("QWERTZUIOASDFGHJKPYXCVBNML");
593 }
594 
595 /// Predefined the simplest plugboard which does not substitute.
596 auto plugboardDoNothing() pure
597 {
598     return plugboard("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
599 }
600 
601 /// Predefined existent reflectors.
602 auto reflectorA() pure
603 {
604     return reflector("EJMZALYXVBWFCRQUONTSPIKHGD");
605 }
606 
607 auto reflectorB() pure
608 {
609     return reflector("YRUHQSLDPXNGOKMIEBFZCWVJAT");
610 }
611 
612 /// ditto
613 auto reflectorC() pure
614 {
615     return reflector("FVPJIAOYEDRZXWGCTKUQSBNMHL");
616 }
617 
618 /// ditto
619 auto reflectorBThin() pure
620 {
621     return reflector("ENKQAUYWJICOPBLMDXZVFTHRGS");
622 }
623 
624 /// ditto
625 auto reflectorCThin() pure
626 {
627     return reflector("RDOBJNTKVEHMLFCWZAXGYIPSUQ");
628 }
629 
630 /// ditto
631 auto reflectorD(dchar ringSetting = 'A') pure
632 {
633     return reflector("IMETCGFRAYSQBZXWLHKDVUPOJN", ringSetting);
634 }
635 
636 /// ditto
637 alias reflectorK = reflectorD;
638 
639 // Double stepping test (http://www.cryptomuseum.com/crypto/enigma/working.htm)
640 unittest
641 {
642     auto m3 = EnigmaM3(plugboardDoNothing, entryWheelABC, rotorI, rotorII, rotorIII, reflectorB, "ODA");
643 
644     assert(m3.rotationStates == [14, 3, 0]);
645 
646     assert(m3('A') == 'H');
647     assert(m3.rotationStates == [15, 3, 0]);
648 
649     assert(m3('A') == 'D');
650     assert(m3.rotationStates == [16, 3, 0]);
651 
652     assert(m3('A') == 'Z');
653     assert(m3.rotationStates == [17, 4, 0]);
654 
655     assert(m3('A') == 'G');
656     assert(m3.rotationStates == [18, 5, 1]);
657 
658     assert(m3('A') == 'O');
659     assert(m3.rotationStates == [19, 5, 1]);
660 
661     assert(m3('A') == 'V');
662     assert(m3.rotationStates == [20, 5, 1]);
663 }
664 
665 // The noches are fixed to the ring.
666 unittest
667 {
668     auto ed = EnigmaD(entryWheelQWE, rotorID, rotorIID, rotorIIID, reflectorD, "UDN" /*!*/ , 'B');
669 
670     assert(ed.rotationStates == [20, 3, 13]);
671 
672     assert(ed('A') == 'Z');
673     assert(ed.rotationStates == [21, 3, 13]);
674 
675     assert(ed('A') == 'D');
676     assert(ed.rotationStates == [22, 3, 13]);
677 
678     assert(ed('A') == 'V');
679     assert(ed.rotationStates == [23, 3, 13]);
680 
681     assert(ed('A') == 'I');
682     assert(ed.rotationStates == [24, 3, 13]);
683 
684     assert(ed('A') == 'C');
685     assert(ed.rotationStates == [25, 4, 13]);
686 
687     assert(ed('A') == 'Z');
688     assert(ed.rotationStates == [0, 5, 14]);
689 
690 
691     // The K's rotor positions are same as the D's.
692     auto sk = SwissK(entryWheelQWE, rotorIK('Z'), rotorIIK('Y'), rotorIIIK, reflectorK('E'), "UDN" /*!*/ , 'X');
693 
694     assert(sk.rotationStates == [20, 3, 13]);
695 
696     assert(sk('A') == 'Y');
697     assert(sk.rotationStates == [21, 3, 13]);
698 
699     assert(sk('A') == 'H');
700     assert(sk.rotationStates == [22, 3, 13]);
701 
702     assert(sk('A') == 'U');
703     assert(sk.rotationStates == [23, 3, 13]);
704 
705     assert(sk('A') == 'M');
706     assert(sk.rotationStates == [24, 3, 13]);
707 
708     assert(sk('A') == 'V');
709     assert(sk.rotationStates == [25, 4, 13]);
710 
711     assert(sk('A') == 'Q');
712     assert(sk.rotationStates == [0, 5, 14]);
713 }
714 
715 /// Step-by-step enciphering.
716 unittest
717 {
718     immutable pbCI = plugboard("ABIDEFGHCJKLMNOPQRSTUVWXYZ"); // C <-> I
719     immutable enWh = entryWheel("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
720     immutable refB = reflector("YRUHQSLDPXNGOKMIEBFZCWVJAT");
721     immutable rot1 = rotor("EKMFLGDQVZNTOWYHXUSPAIBRCJ", 'Q', 'A');
722     immutable rot2 = rotor("AJDKSIRUXBLHWTMCQGZNPYFVOE", 'E', 'B');
723     immutable rot3 = rotor("BDFHJLCPRTXVZNYEIWGAKMUSQO", 'V', 'A');
724 
725     auto e3 = Enigma!3(pbCI, enWh, rot1, rot2, rot3, refB, ['X', 'Q', 'E']);
726     assert(e3('A') == 'K');
727     assert(e3('a') == 'T'); // A lowercase is automatically converted to an uppercase.
728     assert(e3('5') == '5'); // A non-alphabetical character does not changes
729     assert(e3('Ü') == 'Ü'); // the machine state and will be output as it is.
730     assert(e3('A') == 'Q');
731 }
732 
733 /// Encipherment is decipherment.
734 unittest
735 {
736     // These have the same settings.
737     auto encipherer = Enigma!2(entryWheelABC, rotorVI, rotorVII, reflectorC, "PY");
738     auto decipherer = Enigma!2(entryWheelABC, rotorVI, rotorVII, reflectorC, "PY");
739 
740     foreach (dchar c; "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
741     {
742         auto enciphered = encipherer(c);
743         auto deciphered = decipherer(enciphered);
744     }
745 }
746 
747 /++
748  + A certain equivalence of the M3 and the M4.
749  +/
750 unittest
751 {
752     // These have the equivalent settings.
753     auto m3 = EnigmaM3(entryWheelABC, rotorIII, rotorII, rotorI, reflectorB /*!*/ ,
754         "FOO");
755     auto m4 = EnigmaM4(entryWheelABC, rotorIII, rotorII, rotorI,
756         rotorBeta('A') /*!*/ , reflectorBThin /*!*/ , "FOOA" /*!*/ ); // FOO*A*
757 
758     // If each machine has just one movable rotor...
759     auto e1 = Enigma!1(entryWheelABC, rotorI, reflectorC /*!*/ , "X");
760     auto e2fixed = Enigma!(2, true  /*!*/ )(entryWheelABC, rotorI,
761         rotorGamma('A') /*!*/ , reflectorCThin /*!*/ , "XA" /*!*/ ); // X*A*
762 
763     foreach (dchar c; "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
764     {
765         assert(m3(c) == m4(c));
766         assert(e1(c) == e2fixed(c));
767     }
768 }
769 
770 /// Encipher with the M4 and decipher with the equivalent M3.
771 unittest
772 {
773     import std.algorithm.comparison : equal;
774     import std.algorithm.iteration : each, map;
775     import std.array : appender;
776 
777     // These have the equivalent settings.
778     auto m4 = EnigmaM4(plugboard("SBCDEGFHIJKLMNOPQRATUVWXYZ"), entryWheelABC, rotorIII('Y'),
779         rotorII('V'), rotorI('R'), rotorBeta, reflectorBThin, "UEQA");
780     auto m3 = EnigmaM3(plugboard("SBCDEGFHIJKLMNOPQRATUVWXYZ"), entryWheelABC, rotorIII('Y'),
781         rotorII('V'), rotorI('R'), reflectorB, "UEQ");
782 
783     auto enciphered = appender!dstring;
784     "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map!m4.each!(c => enciphered.put(c));
785     assert(enciphered.data == "RIIGSIBEBIZKCTZSSDGQMLSVUX");
786 
787     auto deciphered = enciphered.data.map!m3;
788     assert(deciphered.equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
789 }