enigma_machine 0.0.1.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Rakefile +7 -0
- data/enigma_machine.gemspec +21 -0
- data/lib/enigma_machine/plugboard.rb +31 -0
- data/lib/enigma_machine/reflector.rb +22 -0
- data/lib/enigma_machine/rotor.rb +70 -0
- data/lib/enigma_machine/version.rb +3 -0
- data/lib/enigma_machine.rb +48 -0
- data/spec/enigma_machine_spec.rb +114 -0
- data/spec/integration_spec.rb +74 -0
- data/spec/plugboard_spec.rb +50 -0
- data/spec/reflector_spec.rb +85 -0
- data/spec/rotor_spec.rb +180 -0
- data/spec/spec_helper.rb +14 -0
- metadata +86 -0
data/.rspec
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "enigma_machine/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "enigma_machine"
|
7
|
+
s.version = EnigmaMachine::VERSION
|
8
|
+
s.authors = ["Alex Tomlins"]
|
9
|
+
s.email = ["alex@tomlins.org.uk"]
|
10
|
+
s.homepage = "https://github.com/alext/enigma_machine"
|
11
|
+
s.summary = %q{Enigma machine simulator}
|
12
|
+
s.description = %q{Enigma machine simulator}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
s.add_development_dependency "rake"
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class EnigmaMachine
|
2
|
+
class Plugboard
|
3
|
+
def initialize(mapping_pairs, decorated)
|
4
|
+
build_mapping(mapping_pairs)
|
5
|
+
@decorated = decorated
|
6
|
+
end
|
7
|
+
|
8
|
+
def translate(letter)
|
9
|
+
step = substitute(letter)
|
10
|
+
step = @decorated.translate(step)
|
11
|
+
substitute(step)
|
12
|
+
end
|
13
|
+
|
14
|
+
def substitute(letter)
|
15
|
+
@mapping[letter] || letter
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build_mapping(letter_pairs)
|
21
|
+
@mapping = {}
|
22
|
+
letter_pairs.each do |pair|
|
23
|
+
raise ConfigurationError unless pair =~ /\A[A-Z]{2}\z/
|
24
|
+
a, b = pair.split('')
|
25
|
+
raise ConfigurationError if @mapping[a] or @mapping[b]
|
26
|
+
@mapping[a] = b
|
27
|
+
@mapping[b] = a
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class EnigmaMachine
|
2
|
+
class Reflector < Plugboard
|
3
|
+
STANDARD_MAPPINGS = {
|
4
|
+
:A => %w(AE BJ CM DZ FL GY HX IV KW NR OQ PU ST),
|
5
|
+
:B => %w(AY BR CU DH EQ FS GL IP JX KN MO TZ VW),
|
6
|
+
:C => %w(AF BV CP DJ EI GO HY KR LZ MX NW TQ SU),
|
7
|
+
:Bthin => %w(AE BN CK DQ FU GY HW IJ LO MP RX SZ TV),
|
8
|
+
:Cthin => %w(AR BD CO EJ FN GT HK IV LM PW QZ SX UY),
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(mapping)
|
12
|
+
if mapping.is_a?(Symbol)
|
13
|
+
raise ConfigurationError unless STANDARD_MAPPINGS.has_key?(mapping)
|
14
|
+
mapping = STANDARD_MAPPINGS[mapping]
|
15
|
+
end
|
16
|
+
raise ConfigurationError unless mapping.length == 13
|
17
|
+
build_mapping(mapping)
|
18
|
+
end
|
19
|
+
|
20
|
+
alias :translate :substitute
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class EnigmaMachine
|
2
|
+
class Rotor
|
3
|
+
STANDARD_ROTORS = {
|
4
|
+
:i => "EKMFLGDQVZNTOWYHXUSPAIBRCJ_A",
|
5
|
+
:ii => "AJDKSIRUXBLHWTMCQGZNPYFVOE_A",
|
6
|
+
:iii => "BDFHJLCPRTXVZNYEIWGAKMUSQO_A",
|
7
|
+
:iv => "ESOVPZJAYQUIRHXLNFTGKDCMWB_A",
|
8
|
+
:v => "VZBRGITYUPSDNHLXAWMJQOFECK_A",
|
9
|
+
:vi => "JPGVOUMFYQBENHZRDKASXLICTW_A",
|
10
|
+
:vii => "NZJHGRCXMYSWBOUFAIVLPEKQDT_A",
|
11
|
+
:viii => "FKQHTLXOCBJSPDZRAMEWNIUYGV_A",
|
12
|
+
:beta => "LEYJVCNIXWPBQMDRTAKZGFUHOS_A",
|
13
|
+
:gamma => "FSOKANUERHMBTIYCWLQPZXVGJD_A",
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(rotor_spec, ring_setting, decorated)
|
17
|
+
if rotor_spec.is_a?(Symbol)
|
18
|
+
raise ConfigurationError unless STANDARD_ROTORS.has_key?(rotor_spec)
|
19
|
+
rotor_spec = STANDARD_ROTORS[rotor_spec]
|
20
|
+
end
|
21
|
+
mapping, step_points = rotor_spec.split('_', 2)
|
22
|
+
@mapping = mapping.each_char.map {|c| ALPHABET.index(c) }
|
23
|
+
@ring_offset = ring_setting - 1
|
24
|
+
@decorated = decorated
|
25
|
+
self.position = 'A'
|
26
|
+
end
|
27
|
+
|
28
|
+
def position=(letter)
|
29
|
+
@position = ALPHABET.index(letter)
|
30
|
+
end
|
31
|
+
def position
|
32
|
+
ALPHABET[@position]
|
33
|
+
end
|
34
|
+
|
35
|
+
def advance_position
|
36
|
+
@position = (@position + 1).modulo(26)
|
37
|
+
end
|
38
|
+
|
39
|
+
def forward(letter)
|
40
|
+
index = add_offset ALPHABET.index(letter)
|
41
|
+
new_index = sub_offset @mapping[index]
|
42
|
+
ALPHABET[new_index]
|
43
|
+
end
|
44
|
+
|
45
|
+
def reverse(letter)
|
46
|
+
index = add_offset ALPHABET.index(letter)
|
47
|
+
new_index = sub_offset @mapping.index(index)
|
48
|
+
ALPHABET[new_index]
|
49
|
+
end
|
50
|
+
|
51
|
+
def translate(input)
|
52
|
+
step = forward(input)
|
53
|
+
step = @decorated.translate(step)
|
54
|
+
reverse(step)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def rotor_offset
|
60
|
+
@position - @ring_offset
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_offset(number)
|
64
|
+
(number + rotor_offset).modulo(26)
|
65
|
+
end
|
66
|
+
def sub_offset(number)
|
67
|
+
(number - rotor_offset).modulo(26)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class EnigmaMachine
|
2
|
+
ConfigurationError = Class.new(StandardError)
|
3
|
+
|
4
|
+
ALPHABET = ('A'..'Z').to_a
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@reflector = Reflector.new config[:reflector]
|
8
|
+
@rotors = []
|
9
|
+
config[:rotors].inject(@reflector) do |previous, rotor_config|
|
10
|
+
Rotor.new(*rotor_config, previous).tap {|r| @rotors << r }
|
11
|
+
end
|
12
|
+
@plugboard = Plugboard.new(config[:plug_pairs] || [], @rotors.last)
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_rotors(*positions)
|
16
|
+
positions.each_with_index do |position, i|
|
17
|
+
@rotors[i].position = position
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def press_key(letter)
|
22
|
+
advance_rotors
|
23
|
+
@plugboard.translate(letter)
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate(message)
|
27
|
+
message.upcase.each_char.map do |letter|
|
28
|
+
case letter
|
29
|
+
when /[A-Z]/
|
30
|
+
press_key(letter)
|
31
|
+
when ' '
|
32
|
+
' '
|
33
|
+
end
|
34
|
+
end.join
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def advance_rotors
|
40
|
+
# Temporarily only advance right rotor
|
41
|
+
@rotors[-1].advance_position
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require 'enigma_machine/version'
|
46
|
+
require 'enigma_machine/plugboard'
|
47
|
+
require 'enigma_machine/rotor'
|
48
|
+
require 'enigma_machine/reflector'
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EnigmaMachine do
|
4
|
+
|
5
|
+
describe "creating an instance" do
|
6
|
+
it "should construct the reflector, rotors and plug_board and connect them together" do
|
7
|
+
EnigmaMachine::Reflector.should_receive(:new).with(:foo).and_return(:reflector)
|
8
|
+
EnigmaMachine::Rotor.should_receive(:new).with(:i, 1, :reflector).and_return(:left_rotor)
|
9
|
+
EnigmaMachine::Rotor.should_receive(:new).with(:ii, 2, :left_rotor).and_return(:middle_rotor)
|
10
|
+
EnigmaMachine::Rotor.should_receive(:new).with(:iii, 3, :middle_rotor).and_return(:right_rotor)
|
11
|
+
EnigmaMachine::Plugboard.should_receive(:new).with([1,2,3], :right_rotor).and_return(:plugboard)
|
12
|
+
|
13
|
+
EnigmaMachine.new(:reflector => :foo, :rotors => [[:i, 1], [:ii, 2], [:iii, 3]], :plug_pairs => [1,2,3])
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should create a null plugboard if none specified" do
|
17
|
+
EnigmaMachine::Reflector.stub!(:new)
|
18
|
+
EnigmaMachine::Rotor.stub!(:new)
|
19
|
+
EnigmaMachine::Plugboard.should_receive(:new).with([], anything())
|
20
|
+
|
21
|
+
EnigmaMachine.new(:reflector => :foo, :rotors => [[:i, 1], [:ii, 2], [:iii, 3]])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "setting rotor positions" do
|
26
|
+
before :each do
|
27
|
+
@left_rotor = mock("Rotor")
|
28
|
+
@middle_rotor = mock("Rotor")
|
29
|
+
@right_rotor = mock("Rotor")
|
30
|
+
EnigmaMachine::Rotor.stub!(:new).with(:i, anything(), anything()).and_return(@left_rotor)
|
31
|
+
EnigmaMachine::Rotor.stub!(:new).with(:ii, anything(), anything()).and_return(@middle_rotor)
|
32
|
+
EnigmaMachine::Rotor.stub!(:new).with(:iii, anything(), anything()).and_return(@right_rotor)
|
33
|
+
EnigmaMachine::Reflector.stub!(:new)
|
34
|
+
EnigmaMachine::Plugboard.stub!(:new)
|
35
|
+
|
36
|
+
@e = EnigmaMachine.new(:rotors => [[:i,1], [:ii,2], [:iii,3]])
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should set the position of each rotor" do
|
40
|
+
@left_rotor.should_receive(:position=).with('A')
|
41
|
+
@middle_rotor.should_receive(:position=).with('B')
|
42
|
+
@right_rotor.should_receive(:position=).with('C')
|
43
|
+
|
44
|
+
@e.set_rotors('A', 'B', 'C')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "processing a message" do
|
49
|
+
before :each do
|
50
|
+
EnigmaMachine::Reflector.stub!(:new)
|
51
|
+
EnigmaMachine::Rotor.stub!(:new)
|
52
|
+
EnigmaMachine::Plugboard.stub!(:new)
|
53
|
+
|
54
|
+
@e = EnigmaMachine.new(:rotors => [:a, :b, :c])
|
55
|
+
@e.stub!(:press_key).and_return('Z')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should call press_key for each letter in order, and return the results" do
|
59
|
+
@e.should_receive(:press_key).with('A').ordered.and_return('B')
|
60
|
+
@e.should_receive(:press_key).with('B').ordered.and_return('C')
|
61
|
+
@e.should_receive(:press_key).with('C').ordered.and_return('D')
|
62
|
+
|
63
|
+
@e.translate('ABC').should == 'BCD'
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should pass through spaces unmodified" do
|
67
|
+
@e.should_not_receive(:press_key).with(' ')
|
68
|
+
|
69
|
+
@e.translate('ABC DEF').should == 'ZZZ ZZZ'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should upcase the input before passing to press_key" do
|
73
|
+
@e.should_receive(:press_key).with('A').ordered.and_return('B')
|
74
|
+
@e.should_receive(:press_key).with('B').ordered.and_return('C')
|
75
|
+
@e.should_receive(:press_key).with('C').ordered.and_return('D')
|
76
|
+
|
77
|
+
@e.translate('aBc').should == 'BCD'
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should discard any other characters passed in" do
|
81
|
+
@e.should_not_receive(:press_key).with(/[^A-Z]/)
|
82
|
+
|
83
|
+
@e.translate('A1B3C.D+E%F123').should == 'ZZZZZZ'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "processing a letter" do
|
88
|
+
before :each do
|
89
|
+
EnigmaMachine::Reflector.stub!(:new)
|
90
|
+
EnigmaMachine::Rotor.stub!(:new)
|
91
|
+
@plugboard = mock("Plugboard", :translate => 'B')
|
92
|
+
EnigmaMachine::Plugboard.stub!(:new).and_return(@plugboard)
|
93
|
+
|
94
|
+
@e = EnigmaMachine.new(:rotors => [:a, :b, :c])
|
95
|
+
@e.stub!(:advance_rotors)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should advance the rotors" do
|
99
|
+
@e.should_receive(:advance_rotors)
|
100
|
+
|
101
|
+
@e.press_key('A')
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should call translate on the plugboard, and return the result" do
|
105
|
+
@plugboard.should_receive(:translate).and_return('F')
|
106
|
+
|
107
|
+
@e.press_key('A').should == 'F'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "advancing rotors" do
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Integration tests" do
|
4
|
+
|
5
|
+
describe "basic sample tests" do
|
6
|
+
# taken from http://wiki.franklinheath.co.uk/index.php/Enigma/Paper_Enigma
|
7
|
+
|
8
|
+
it "should translate a message that only needs the right rotor to advance" do
|
9
|
+
e = EnigmaMachine.new(
|
10
|
+
:reflector => :B,
|
11
|
+
:rotors => [[:i, 1], [:ii, 1], [:iii, 1]]
|
12
|
+
)
|
13
|
+
e.set_rotors('A', 'B', 'C')
|
14
|
+
|
15
|
+
e.translate('AEFAE JXXBN XYJTY').should == 'CONGR ATULA TIONS'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "real world sample messages" do
|
21
|
+
describe "Enigma I/M3" do
|
22
|
+
# Examples taken from http://wiki.franklinheath.co.uk/index.php/Enigma/Sample_Messages
|
23
|
+
specify "Enigma Instruction Manual 1930" do
|
24
|
+
e = EnigmaMachine.new(
|
25
|
+
:reflector => :A,
|
26
|
+
:rotors => [[:ii, 24], [:i, 13], [:iii, 22]],
|
27
|
+
:plug_pairs => %w(AM FI NV PS TU WZ)
|
28
|
+
)
|
29
|
+
|
30
|
+
e.set_rotors('A', 'B', 'L')
|
31
|
+
result = e.translate('GCDSE AHUGW TQGRK VLFGX UCALX VYMIG MMNMF DXTGN VHVRM MEVOU YFZSL RHDRR XFJWC FHUHM UNZEF RDISI KBGPM YVXUZ')
|
32
|
+
|
33
|
+
result.should == 'FEIND LIQEI NFANT ERIEK OLONN EBEOB AQTET XANFA NGSUE DAUSG ANGBA ERWAL DEXEN DEDRE IKMOS TWAER TSNEU STADT'
|
34
|
+
# German: Feindliche Infanterie Kolonne beobachtet. Anfang Südausgang Bärwalde. Ende 3km ostwärts Neustadt.
|
35
|
+
# English: Enemy infantry column was observed. Beginning [at] southern exit [of] Baerwalde. Ending 3km east of Neustadt.
|
36
|
+
end
|
37
|
+
|
38
|
+
specify "Operation Barbarossa, 1941" do
|
39
|
+
e = EnigmaMachine.new(
|
40
|
+
:reflector => :B,
|
41
|
+
:rotors => [[:ii, 2], [:iv, 21], [:v, 12]],
|
42
|
+
:plug_pairs => %w(AV BS CG DL FU HZ IN KM OW RX)
|
43
|
+
)
|
44
|
+
|
45
|
+
e.set_rotors('B', 'L', 'A')
|
46
|
+
result = e.translate('EDPUD NRGYS ZRCXN UYTPO MRMBO FKTBZ REZKM LXLVE FGUEY SIOZV EQMIK UBPMM YLKLT TDEIS MDICA GYKUA CTCDO MOHWX MUUIA UBSTS LRNBZ SZWNR FXWFY SSXJZ VIJHI DISHP RKLKA YUPAD TXQSP INQMA TLPIF SVKDA SCTAC DPBOP VHJK-')
|
47
|
+
result.should == 'AUFKL XABTE ILUNG XVONX KURTI NOWAX KURTI NOWAX NORDW ESTLX SEBEZ XSEBE ZXUAF FLIEG ERSTR ASZER IQTUN GXDUB ROWKI XDUBR OWKIX OPOTS CHKAX OPOTS CHKAX UMXEI NSAQT DREIN ULLXU HRANG ETRET ENXAN GRIFF XINFX RGTX-'
|
48
|
+
|
49
|
+
e.set_rotors('L', 'S', 'D')
|
50
|
+
result = e.translate('SFBWD NJUSE GQOBH KRTAR EEZMW KPPRB XOHDR OEQGB BGTQV PGVKB VVGBI MHUSZ YDAJQ IROAX SSSNR EHYGG RPISE ZBOVM QIEMM ZCYSG QDGRE RVBIL EKXYQ IRGIR QNRDN VRXCY YTNJR')
|
51
|
+
result.should == 'DREIG EHTLA NGSAM ABERS IQERV ORWAE RTSXE INSSI EBENN ULLSE QSXUH RXROE MXEIN SXINF RGTXD REIXA UFFLI EGERS TRASZ EMITA NFANG XEINS SEQSX KMXKM XOSTW XKAME NECXK'
|
52
|
+
|
53
|
+
# German: Aufklärung abteilung von Kurtinowa nordwestlich Sebez [auf] Fliegerstraße in Richtung Dubrowki, Opotschka. Um 18:30 Uhr angetreten angriff. Infanterie Regiment 3 geht langsam aber sicher vorwärts. 17:06 Uhr röm eins InfanterieRegiment 3 auf Fliegerstraße mit Anfang 16km ostwärts Kamenec.
|
54
|
+
# English: Reconnaissance division from Kurtinowa north-west of Sebezh on the flight corridor towards Dubrowki, Opochka. Attack begun at 18:30 hours. Infantry Regiment 3 goes slowly but surely forwards. 17:06 hours [Roman numeral I?] Infantry Regiment 3 on the flight corridor starting 16 km east of Kamenec.
|
55
|
+
end
|
56
|
+
|
57
|
+
specify "Scharnhorst (Konteradmiral Erich Bey), 1943" do
|
58
|
+
e = EnigmaMachine.new(
|
59
|
+
:reflector => :B,
|
60
|
+
:rotors => [[:iii, 1], [:vi, 8], [:viii, 13]],
|
61
|
+
:plug_pairs => %w(AN EZ HK IJ LR MQ OT PV SW UX)
|
62
|
+
)
|
63
|
+
|
64
|
+
e.set_rotors('U', 'Z', 'V')
|
65
|
+
result = e.translate('YKAE NZAP MSCH ZBFO CUVM RMDP YCOF HADZ IZME FXTH FLOL PZLF GGBO TGOX GRET DWTJ IQHL MXVJ WKZU ASTR')
|
66
|
+
result.should == 'STEUE REJTA NAFJO RDJAN STAND ORTQU AAACC CVIER NEUNN EUNZW OFAHR TZWON ULSMX XSCHA RNHOR STHCO'
|
67
|
+
|
68
|
+
# German: Steuere Tanafjord an. Standort Quadrat AC4992, fahrt 20sm. Scharnhorst. [hco - padding?]
|
69
|
+
# English: Heading for Tanafjord. Position is square AC4992, speed 20 knots. Scharnhorst.
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EnigmaMachine::Plugboard do
|
4
|
+
|
5
|
+
describe "configuring a plugboard" do
|
6
|
+
it "should raise an error if passing in anything other than an array of upper case letter pairs" do
|
7
|
+
lambda do
|
8
|
+
EnigmaMachine::Plugboard.new(%w(AB CDE FG), :decorated)
|
9
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
10
|
+
|
11
|
+
lambda do
|
12
|
+
EnigmaMachine::Plugboard.new(%w(AB cd FG), :decorated)
|
13
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should raise an error if attempting to connect more than one wire to a letter" do
|
17
|
+
lambda do
|
18
|
+
EnigmaMachine::Plugboard.new(%w(AB CD GC), :decorated)
|
19
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "substituting a letter" do
|
24
|
+
before :each do
|
25
|
+
@p = EnigmaMachine::Plugboard.new(%w(AF DG EX), :decorated)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should substitute a letter that has a plug wire connected" do
|
29
|
+
@p.substitute('D').should == 'G'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should substitute a letter that's on the other end of the wire" do
|
33
|
+
@p.substitute('X').should == 'E'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should pass through a letter that doesn't have a plug wire connected" do
|
37
|
+
@p.substitute('C').should == 'C'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "decorating a rotor" do
|
42
|
+
it "should substitute the latter, pass to the rotor, then substiture the final result" do
|
43
|
+
rotor = stub("Rotor")
|
44
|
+
rotor.should_receive(:translate).with('F').and_return('H')
|
45
|
+
|
46
|
+
plugboard = EnigmaMachine::Plugboard.new(%w(AF DG EX ZH), rotor)
|
47
|
+
plugboard.translate('A').should == 'Z'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EnigmaMachine::Reflector do
|
4
|
+
|
5
|
+
describe "configuring a reflector" do
|
6
|
+
it "should raise an error if passing in anything other than an array of upper case letter pairs" do
|
7
|
+
lambda do
|
8
|
+
EnigmaMachine::Reflector.new(%w(AY BRC U DH EQ FS GL IP JX KN MO TZ VW))
|
9
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
10
|
+
|
11
|
+
lambda do
|
12
|
+
EnigmaMachine::Reflector.new(%w(AY BR cu DH EQ FS GL IP JX KN MO TZ VW))
|
13
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should raise an error if attempting to connect more than one wire to a letter" do
|
17
|
+
lambda do
|
18
|
+
EnigmaMachine::Reflector.new(%w(AY BR CB DH EQ FS GL IP JX KN MO TZ VW))
|
19
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should raise an error unless all 26 letters are configured" do
|
23
|
+
lambda do
|
24
|
+
EnigmaMachine::Reflector.new(%w(AY BR CU DH EQ FS GL IP JX KN MO TZ))
|
25
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "using one of the standard configurations" do
|
29
|
+
it "should raise an error if using an unknown name" do
|
30
|
+
lambda do
|
31
|
+
EnigmaMachine::Reflector.new(:foo)
|
32
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should support reflector A" do
|
36
|
+
r = EnigmaMachine::Reflector.new(:A)
|
37
|
+
r.translate('A').should == 'E'
|
38
|
+
r.translate('F').should == 'L'
|
39
|
+
r.translate('Q').should == 'O'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should support reflector B" do
|
43
|
+
r = EnigmaMachine::Reflector.new(:B)
|
44
|
+
r.translate('A').should == 'Y'
|
45
|
+
r.translate('F').should == 'S'
|
46
|
+
r.translate('Q').should == 'E'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should support reflector C" do
|
50
|
+
r = EnigmaMachine::Reflector.new(:C)
|
51
|
+
r.translate('A').should == 'F'
|
52
|
+
r.translate('K').should == 'R'
|
53
|
+
r.translate('Q').should == 'T'
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should support reflector Bthin" do
|
57
|
+
r = EnigmaMachine::Reflector.new(:Bthin)
|
58
|
+
r.translate('A').should == 'E'
|
59
|
+
r.translate('F').should == 'U'
|
60
|
+
r.translate('P').should == 'M'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should support reflector Cthin" do
|
64
|
+
r = EnigmaMachine::Reflector.new(:Cthin)
|
65
|
+
r.translate('A').should == 'R'
|
66
|
+
r.translate('K').should == 'H'
|
67
|
+
r.translate('Q').should == 'Z'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "translating letters" do
|
73
|
+
before :each do
|
74
|
+
@reflector = EnigmaMachine::Reflector.new(%w(AY BR CU DH EQ FS GL IP JX KN MO TZ VW))
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should substitute a letter that's first in a pair" do
|
78
|
+
@reflector.translate('B').should == 'R'
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should substitute a letter that's second in a pair" do
|
82
|
+
@reflector.translate('L').should == 'G'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/rotor_spec.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EnigmaMachine::Rotor do
|
4
|
+
|
5
|
+
describe "configuring a rotor" do
|
6
|
+
describe "using one of the standard configurations" do
|
7
|
+
it "should raise an error if using an unknown name" do
|
8
|
+
lambda do
|
9
|
+
EnigmaMachine::Rotor.new(:foo, 1, :next)
|
10
|
+
end.should raise_error(EnigmaMachine::ConfigurationError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should support rotor i" do
|
14
|
+
r = EnigmaMachine::Rotor.new(:i, 1, :next)
|
15
|
+
r.forward('A').should == 'E'
|
16
|
+
r.forward('Q').should == 'X'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should support rotor ii" do
|
20
|
+
r = EnigmaMachine::Rotor.new(:ii, 1, :next)
|
21
|
+
r.forward('A').should == 'A'
|
22
|
+
r.forward('M').should == 'W'
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should support rotor iii" do
|
26
|
+
r = EnigmaMachine::Rotor.new(:iii, 1, :next)
|
27
|
+
r.forward('A').should == 'B'
|
28
|
+
r.forward('Q').should == 'I'
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should support rotor iv" do
|
32
|
+
r = EnigmaMachine::Rotor.new(:iv, 1, :next)
|
33
|
+
r.forward('A').should == 'E'
|
34
|
+
r.forward('Q').should == 'N'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should support rotor v" do
|
38
|
+
r = EnigmaMachine::Rotor.new(:v, 1, :next)
|
39
|
+
r.forward('A').should == 'V'
|
40
|
+
r.forward('Q').should == 'A'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should support rotor vi" do
|
44
|
+
r = EnigmaMachine::Rotor.new(:vi, 1, :next)
|
45
|
+
r.forward('A').should == 'J'
|
46
|
+
r.forward('Q').should == 'D'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should support rotor vii" do
|
50
|
+
r = EnigmaMachine::Rotor.new(:vii, 1, :next)
|
51
|
+
r.forward('A').should == 'N'
|
52
|
+
r.forward('Q').should == 'A'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should support rotor viii" do
|
56
|
+
r = EnigmaMachine::Rotor.new(:viii, 1, :next)
|
57
|
+
r.forward('A').should == 'F'
|
58
|
+
r.forward('Q').should == 'A'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should support rotor beta" do
|
62
|
+
r = EnigmaMachine::Rotor.new(:beta, 1, :next)
|
63
|
+
r.forward('A').should == 'L'
|
64
|
+
r.forward('Q').should == 'T'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should support rotor gamma" do
|
68
|
+
r = EnigmaMachine::Rotor.new(:gamma, 1, :next)
|
69
|
+
r.forward('A').should == 'F'
|
70
|
+
r.forward('Q').should == 'W'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
describe "setting and manipulating rotor positions" do
|
75
|
+
before :each do
|
76
|
+
@rotor = EnigmaMachine::Rotor.new("ABCD", 1, :foo)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should set the position to 'A' by default" do
|
80
|
+
@rotor.position.should == 'A'
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should allow setting the position" do
|
84
|
+
@rotor.position = 'G'
|
85
|
+
@rotor.position.should == 'G'
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "advancing the position" do
|
89
|
+
it "should allow advancing the position" do
|
90
|
+
@rotor.advance_position
|
91
|
+
@rotor.position.should == 'B'
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should wrap around when advancing beyond 'Z'" do
|
95
|
+
@rotor.position = 'Z'
|
96
|
+
@rotor.advance_position
|
97
|
+
@rotor.position.should == 'A'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "forward and reverse translation" do
|
103
|
+
context "with a ring-setting of 1 (no adjustment), and a rotor position of A (the default)" do
|
104
|
+
before :each do
|
105
|
+
@rotor = EnigmaMachine::Rotor.new("EKMFLGDQVZNTOWYHXUSPAIBRCJ_R", 1, :decorated)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should translate letters correctly in the forward direction" do
|
109
|
+
@rotor.forward("B").should == "K"
|
110
|
+
@rotor.forward("Y").should == "C"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should translate letters correctly in the reverse direction" do
|
114
|
+
@rotor.reverse("L").should == "E"
|
115
|
+
@rotor.reverse("C").should == "Y"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "with a ring-setting of 5, and a rotor position of A (the default)" do
|
120
|
+
before :each do
|
121
|
+
@rotor = EnigmaMachine::Rotor.new("EKMFLGDQVZNTOWYHXUSPAIBRCJ_R", 5, :decorated)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should translate letters correctly in the forward direction" do
|
125
|
+
@rotor.forward("B").should == "V"
|
126
|
+
@rotor.forward("U").should == "B"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should translate letters correctly in the reverse direction" do
|
130
|
+
@rotor.reverse("F").should == "A"
|
131
|
+
@rotor.reverse("C").should == "S"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "with a ring-setting of 1 (no adjustment), and a rotor position of L" do
|
136
|
+
before :each do
|
137
|
+
@rotor = EnigmaMachine::Rotor.new("EKMFLGDQVZNTOWYHXUSPAIBRCJ_R", 1, :decorated)
|
138
|
+
@rotor.position = 'L'
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should translate letters correctly in the forward direction" do
|
142
|
+
@rotor.forward("B").should == "D"
|
143
|
+
@rotor.forward("Y").should == "O"
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should translate letters correctly in the reverse direction" do
|
147
|
+
@rotor.reverse("L").should == "C"
|
148
|
+
@rotor.reverse("V").should == "U"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context "with a ring-setting of 5, and a rotor position of T" do
|
153
|
+
before :each do
|
154
|
+
@rotor = EnigmaMachine::Rotor.new("EKMFLGDQVZNTOWYHXUSPAIBRCJ_R", 5, :decorated)
|
155
|
+
@rotor.position = 'T'
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should translate letters correctly in the forward direction" do
|
159
|
+
@rotor.forward("B").should == "I"
|
160
|
+
@rotor.forward("Y").should == "H"
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should translate letters correctly in the reverse direction" do
|
164
|
+
@rotor.reverse("L").should == "F"
|
165
|
+
@rotor.reverse("V").should == "M"
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "decorating a reflector" do
|
172
|
+
it "should substitute the letter, pass to the rotor, then substitute the final result" do
|
173
|
+
reflector = stub("Reflector")
|
174
|
+
reflector.should_receive(:translate).with('D').and_return('H')
|
175
|
+
|
176
|
+
rotor = EnigmaMachine::Rotor.new("EKMFLGDQVZNTOWYHXUSPAIBRCJ_R", 1, reflector)
|
177
|
+
rotor.translate('G').should == 'P'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
config.run_all_when_everything_filtered = true
|
10
|
+
config.filter_run :focus
|
11
|
+
end
|
12
|
+
|
13
|
+
$: << '../lib'
|
14
|
+
require 'enigma_machine'
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: enigma_machine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.alpha1
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Tomlins
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &13890560 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *13890560
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &13890040 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *13890040
|
36
|
+
description: Enigma machine simulator
|
37
|
+
email:
|
38
|
+
- alex@tomlins.org.uk
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- .rspec
|
45
|
+
- Gemfile
|
46
|
+
- Rakefile
|
47
|
+
- enigma_machine.gemspec
|
48
|
+
- lib/enigma_machine.rb
|
49
|
+
- lib/enigma_machine/plugboard.rb
|
50
|
+
- lib/enigma_machine/reflector.rb
|
51
|
+
- lib/enigma_machine/rotor.rb
|
52
|
+
- lib/enigma_machine/version.rb
|
53
|
+
- spec/enigma_machine_spec.rb
|
54
|
+
- spec/integration_spec.rb
|
55
|
+
- spec/plugboard_spec.rb
|
56
|
+
- spec/reflector_spec.rb
|
57
|
+
- spec/rotor_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
homepage: https://github.com/alext/enigma_machine
|
60
|
+
licenses: []
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
hash: -1944969372318025887
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>'
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 1.3.1
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.8.10
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Enigma machine simulator
|
86
|
+
test_files: []
|