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 ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in enigma_machine.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -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,3 @@
1
+ class EnigmaMachine
2
+ VERSION = "0.0.1.alpha1"
3
+ 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
@@ -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
@@ -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: []