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