rotor_machine 1.0.14 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 443abe9d94a7c609dc9e6e737a9cf134cc7c81d485977b7f32b9672572cacb4f
4
- data.tar.gz: 4f9ac00ff8680eb8c1f10d385d6905a35d97d240da5b9ef209a3a26304f3e2d5
3
+ metadata.gz: b9b724da9efeeb1cb707e1b455a0cca97b9a911ce0b8bf11824e786c044135d6
4
+ data.tar.gz: 844f9bc228874dc922369d00a3e2ae6caec69c14c63491961e21fb5b32988873
5
5
  SHA512:
6
- metadata.gz: 1280cdc56b4e65ea14716c454316cd1c8b4411c92e944230ac67fe526f36f708108afb6a89c0b9f6662afa556e2c54c21035bf780ea0a2e75b45cd4a53e69726
7
- data.tar.gz: f1225743be6ea1947a8d1fc1b0966f42f4686b38d7762cbfbd65baf27ccd0951bc7c64efdb5930d277f7b88f660eb3121cf7a29ddb32b1a506013c3673ccf88f
6
+ metadata.gz: 3da1a7af7b05149bb3ba3bbb2e514e548ea8f6b7727a1b35858a9c381372f01c2cec692e95d20cd0a435b7a21d5cfa1e1f3ec659806aba9862a7c2b7ddfa6900
7
+ data.tar.gz: 04a238d1393e0b9ca931f6af190e1813ee9251b299c829ba1df7928a1a94fc2479149576aaef3505fac2ce02d5b1ba7afa347ed3400a2fc500137e15fdac5f76
data/Rakefile CHANGED
@@ -5,3 +5,13 @@ require "tcravit_ruby_lib/rake_tasks"
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
 
7
7
  task :default => :spec
8
+ require 'rake'
9
+
10
+ namespace :coverage do
11
+ desc "Generate a contextual report from the SimpleCov output"
12
+ task :resolve do
13
+ project_root = File.expand_path(File.join(File.dirname(__FILE__)))
14
+ system("#{project_root}/bin/resolve_coverage.pl #{project_root}/coverage/coverage.txt")
15
+ end
16
+ end
17
+
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/perl -w
2
+ ############################################################################
3
+ # resolve_coverage.pl: Parse the output of a Ruby simplecov_erb text file,
4
+ # and generate a report with code context for missed
5
+ # lines.
6
+ #
7
+ # Version 1.00, 2018-06-15, tammycravit@me.com
8
+ ############################################################################
9
+
10
+ use File::Spec;
11
+ use File::Basename;
12
+ use File::Slurp;
13
+ use Term::ANSIColor qw(:constants);
14
+ use Cwd;
15
+ use Getopt::Long;
16
+
17
+ ####################
18
+ # CONFIGURATION CONSTANTS
19
+ ####################
20
+
21
+ $FILE_PREFIX = "";
22
+ $FILE_SUFFIX = "";
23
+ $EXAMPLE_PREFIX = "";
24
+ $EXAMPLE_SUFFIX = "\n";
25
+ $CONTEXT_SIZE = 2;
26
+ $CONTEXT_UNCOVERED_MARKER = "->";
27
+ $CONTEXT_UNCOVERED_COLOR = RED;
28
+ $FILENAME_COLOR = BLUE;
29
+ $BARE_OUTPUT = undef;
30
+
31
+ $cov_files = 0;
32
+ $cov_lines = 0;
33
+
34
+ ####################
35
+ # Script begins here
36
+ ####################
37
+
38
+ sub generate_file_context
39
+ {
40
+ my ($filename, $lines_list) = @_;
41
+ $filename =~ s@//@/@g;
42
+
43
+ my @lines = @$lines_list;
44
+ @content = read_file($filename, chomp => 1);
45
+
46
+ if (length $FILE_PREFIX) { print $FILE_PREFIX; }
47
+
48
+ if ($BARE_OUTPUT)
49
+ {
50
+ print "*** ", $filename, " ($#content lines)", "\n";
51
+ }
52
+ else
53
+ {
54
+ print
55
+ BOLD, WHITE, "*** ", RESET
56
+ BOLD, $FILENAME_COLOR, $filename, RESET,
57
+ WHITE, " ($#content lines)", RESET,
58
+ "\n";
59
+ }
60
+ print "\n";
61
+
62
+ foreach my $which_line (@lines)
63
+ {
64
+ $which_line--;
65
+ if (length $EXAMPLE_PREFIX) { print $EXAMPLE_PREFIX; }
66
+
67
+ for ($i = $which_line - $CONTEXT_SIZE; $i <= $which_line + $CONTEXT_SIZE; $i++)
68
+ {
69
+ if ($i <= $#content)
70
+ {
71
+ my $color = ($i == $which_line ? $CONTEXT_UNCOVERED_COLOR : WHITE);
72
+ my $number_color = CYAN;
73
+ my $reset = RESET;
74
+ if ($BARE_OUTPUT)
75
+ {
76
+ printf "%s%s%s%5.0d:%s %s%s\n",
77
+ "",
78
+ ($i == $which_line ? $CONTEXT_UNCOVERED_MARKER : (" " x length($CONTEXT_UNCOVERED_MARKER))),
79
+ "",
80
+ $i+1,
81
+ "",
82
+ $content[$i],
83
+ "";
84
+ }
85
+ else
86
+ {
87
+ printf "%s%s%s%5.0d:%s %s%s\n",
88
+ $color,
89
+ ($i == $which_line ? $CONTEXT_UNCOVERED_MARKER : (" " x length($CONTEXT_UNCOVERED_MARKER))),
90
+ $number_color,
91
+ $i+1,
92
+ $color,
93
+ $content[$i],
94
+ $reset;
95
+ }
96
+ }
97
+ }
98
+
99
+ if (length $EXAMPLE_SUFFIX) { print $EXAMPLE_SUFFIX; }
100
+ $cov_lines++;
101
+ }
102
+
103
+ if (length $FILE_SUFFIX) { print $FILE_SUFFIX; }
104
+ $cov_files++;
105
+ }
106
+
107
+ GetOptions(
108
+ "file-prefix=s" => \$FILE_PREFIX,
109
+ "file-suffix=s" => \$FILE_SUFFIX,
110
+ "example-prefix=s" => \$EXAMPLE_PREFIX,
111
+ "example-suffix=s" => \$EXAMPLE_SUFFIX,
112
+ "context-size=i" => \$CONTEXT_SIZE,
113
+ "context-marker=s" => \$CONTEXT_UNCOVERED_MARKER,
114
+ "bare" => \$BARE_OUTPUT,
115
+ );
116
+
117
+ $coverage_file = File::Spec->rel2abs($ARGV[0]);
118
+ $project_root = dirname($coverage_file);
119
+ do
120
+ {
121
+ $project_root = dirname($project_root);
122
+ }
123
+ until ((-d "$project_root/coverage") || ($project_root eq '/'));
124
+ die "Could not find project root starting from $coverage_file\n" if ($project_root eq "/");
125
+
126
+ unless ($BARE_OUTPUT)
127
+ {
128
+ print "****************************************************************************\n";
129
+ print "* resolve_coverage.pl: Parse a simplecov-erb coverage report and generate *\n";
130
+ print "* contextual code snippets for uncovered lines. *\n";
131
+ print "* *\n";
132
+ print "* Version 1.00, 2018-06-15, Tammy Cravit, tammycravit\@me.com *\n";
133
+ print "****************************************************************************\n";
134
+ print "\n";
135
+
136
+ print BOLD, MAGENTA, "==> Coverage file: ", RESET, MAGENTA, $coverage_file, "\n", RESET;
137
+ print BOLD, MAGENTA, "==> Project root : ", RESET, MAGENTA, $project_root, "\n", RESET;
138
+ print BOLD, MAGENTA, "==> Context lines: ", RESET, MAGENTA, $CONTEXT_SIZE, "\n", RESET;
139
+ print "\n";
140
+ }
141
+
142
+
143
+ die "Usage: $0 coverage_file.txt\n" unless (-f $coverage_file);
144
+
145
+ open (COVERAGE, $coverage_file) || die;
146
+ while (<COVERAGE>)
147
+ {
148
+ chomp;
149
+ if ($_ =~ m/^(\S+)\b.*?missed:\s?([0123456789,]+)/)
150
+ {
151
+ $filename = $1;
152
+ @lines = split(/,/, $2);
153
+ generate_file_context("$project_root/$filename", \@lines);
154
+ }
155
+ }
156
+ close (COVERAGE);
157
+
158
+ if ($BARE_OUTPUT)
159
+ {
160
+ print "resolve_coverage.pl processed ", $cov_lines, " examples from ", $cov_files,
161
+ " files.\n";
162
+ }
163
+ else
164
+ {
165
+ print "Done. Processed ",
166
+ CYAN, $cov_lines, RESET,
167
+ " examples from ",
168
+ CYAN, $cov_files, RESET,
169
+ " files.\n";
170
+ }
171
+ exit 0;
data/lib/rotor_machine.rb CHANGED
@@ -3,7 +3,7 @@ $:.unshift File.dirname(__FILE__)
3
3
  require "rotor_machine/version"
4
4
 
5
5
  Dir[File.join(File.dirname(__FILE__), "rotor_machine", "*.rb")].reject { |x| File.basename(x) == "version.rb" }.each do |f|
6
- require File.join("rotor_machine", File.basename(f))
6
+ require File.join("rotor_machine", File.basename(f))
7
7
  end
8
8
 
9
9
  ##
@@ -21,7 +21,7 @@ end
21
21
  # Many thanks to Kevin Sylvestre, whose {https://ksylvest.com/posts/2015-01-03/the-enigma-machine-using-ruby blog post}
22
22
  # helped me understand some aspects of the internal workings of the Enigma
23
23
  # and how the signals flowed through the pieces of the machine.
24
- #
24
+ #
25
25
  #@author Tammy Cravit <tammycravit@me.com>
26
26
  module RotorMachine
27
27
  end
@@ -6,15 +6,52 @@ module RotorMachine
6
6
  module Factory
7
7
  extend self
8
8
 
9
+ ##
10
+ # Generates a default-configuration RotorMachine, with the following
11
+ # state:
12
+ #
13
+ # - Rotors I, II, III, each set to A and configured to advance a single
14
+ # step at a time
15
+ # - Reflector A
16
+ # - An empty plugboard with no connections
17
+ #
18
+ # The {RotorMachine::Machine#default_machine} method calls this factory
19
+ # method, and is maintained there for backward compatibility.
20
+ def default_machine
21
+ m = build_machine(
22
+ rotors: [:ROTOR_I, :ROTOR_II, :ROTOR_III],
23
+ reflector: build_reflector(reflector_kind: :REFLECTOR_A)
24
+ )
25
+ m.set_rotors("AAA")
26
+ return m
27
+ end
28
+
29
+ ##
30
+ # Generates an empty-configuration RotorMachine, with the following
31
+ # state:
32
+ #
33
+ # - No rotors
34
+ # - No reflector
35
+ # - An empty plugboard with no connections
36
+ #
37
+ # A RotorMachine in this state will raise an {ArgumentError} until you
38
+ # outfit it with at least one rotor and a reflector.
39
+ #
40
+ # The {RotorMachine::Machine#default_machine} method calls this factory
41
+ # method, and is maintained there for backward compatibility.
42
+ def empty_machine
43
+ return build_machine()
44
+ end
45
+
9
46
  ##
10
47
  # Build a new {Rotor} and return it.
11
48
  #
12
- # The options hash for this method can accept the following named
49
+ # The options hash for this method can accept the following named
13
50
  # arguments:
14
51
  #
15
52
  # *:rotor_kind* - The type of rotor to create. Should be a symbol matching
16
53
  # a rotor type constant in the {RotorMachine::Rotor} class,
17
- # or a 26-character string giving the letter sequence for
54
+ # or a 26-character string giving the letter sequence for
18
55
  # the rotor. Defaults to *:ROTOR_1* if not specified.
19
56
  #
20
57
  # *:initial_position* - The initial position of the rotor (0-based
@@ -28,16 +65,19 @@ module RotorMachine
28
65
  # rotor.
29
66
  # @return The newly-built rotor.
30
67
  def build_rotor(options={})
31
- rotor_kind = options.fetch(:rotor_kind, :ROTOR_I)
68
+ rotor_kind = options.fetch(:rotor_kind, nil)
32
69
  initial_position = options.fetch(:initial_position, 0)
33
70
  step_size = options.fetch(:step_size, 1)
34
71
 
35
72
  rotor_alphabet = nil
73
+ if rotor_kind.nil?
74
+ raise ArgumentError, "Rotor kind not specified"
75
+ end
36
76
 
37
- if (rotor_kind.class.name == "Symbol")
77
+ if rotor_kind.is_a? Symbol
38
78
  raise ArgumentError, "Invalid rotor kind (symbol #{rotor_kind} not found)" unless RotorMachine::Rotor.constants.include?(rotor_kind)
39
79
  rotor_alphabet = RotorMachine::Rotor.const_get(rotor_kind)
40
- elsif (rotor_kind.class.name == "String")
80
+ elsif rotor_kind.is_a? String
41
81
  raise ArgumentError, "Invalid rotor kind (invalid length)" unless rotor_kind.length == 26
42
82
  rotor_alphabet = rotor_kind.upcase
43
83
  else
@@ -66,12 +106,12 @@ module RotorMachine
66
106
  ##
67
107
  # Build a new {Reflector} and return it.
68
108
  #
69
- # The options hash for this method can accept the following named
109
+ # The options hash for this method can accept the following named
70
110
  # arguments:
71
111
  #
72
112
  # *:reflector_kind* - The type of reflector to create. Should be a symbol matching
73
113
  # a reflector type constant in the {RotorMachine::Reflector} class,
74
- # or a 26-character string giving the letter sequence for
114
+ # or a 26-character string giving the letter sequence for
75
115
  # the reflector. Defaults to *:REFLECTOR_A* if not specified.
76
116
  #
77
117
  # *:initial_position* - The initial position of the reflector (0-based
@@ -82,21 +122,27 @@ module RotorMachine
82
122
  # reflector.
83
123
  # @return The newly-built reflector.
84
124
  def build_reflector(options={})
85
- reflector_kind = options.fetch(:reflector_kind, :REFLECTOR_A)
86
- initial_position = options.fetch(:initial_position, 0)
125
+ reflector_kind = options.fetch(:reflector_kind, nil)
126
+ initial_position = options.fetch(:initial_position, nil)
87
127
 
88
128
  reflector_alphabet = nil
129
+ if reflector_kind.nil?
130
+ raise ArgumentError, "Reflector type not specified"
131
+ end
132
+ if initial_position.nil?
133
+ initial_position = 0
134
+ end
89
135
 
90
- if (reflector_kind.class.name == "Symbol")
136
+ if reflector_kind.is_a? Symbol
91
137
  unless RotorMachine::Reflector.constants.include?(reflector_kind)
92
- raise ArgumentError, "Invalid reflector kind (symbol #{reflector_kind} not found)"
138
+ raise ArgumentError, "Invalid reflector kind (symbol #{reflector_kind} not found)"
93
139
  end
94
140
  reflector_alphabet = RotorMachine::Reflector.const_get(reflector_kind)
95
- elsif (reflector_kind.class.name == "String")
141
+ elsif reflector_kind.is_a? String
96
142
  raise ArgumentError, "Invalid reflector kind (invalid length)" unless reflector_kind.length == 26
97
143
  reflector_alphabet = reflector_kind.upcase
98
144
  else
99
- raise ArgumentError, "Invalid reflector kind (invalid type #{reflector_kind.class.name})"
145
+ raise ArgumentError, "Invalid reflector kind (invalid type)"
100
146
  end
101
147
 
102
148
  if initial_position.is_a? Numeric
@@ -107,7 +153,7 @@ module RotorMachine
107
153
  end
108
154
  initial_position = reflector_alphabet.index(initial_position)
109
155
  else
110
- raise ArgumentError, "Invalid position (invalid type #{initial_position.class.name})"
156
+ raise ArgumentError, "Invalid position (invalid type)"
111
157
  end
112
158
 
113
159
  return RotorMachine::Reflector.new(reflector_alphabet, initial_position)
@@ -149,19 +195,22 @@ module RotorMachine
149
195
 
150
196
  m = RotorMachine::Machine.new()
151
197
  rotors.each do |r|
152
- if r.class.name == "RotorMachine::Rotor"
198
+ if r.is_a? RotorMachine::Rotor
153
199
  m.rotors << r
154
- elsif r.class.name == "Symbol"
200
+ elsif r.is_a? Symbol
155
201
  m.rotors << RotorMachine::Factory.build_rotor(rotor_kind: r)
202
+ else
203
+ raise ArgumentError, "#{r} is not a rotor or a rotor kind symbol"
156
204
  end
157
205
  end
158
206
 
159
207
  unless reflector.nil?
160
- if reflector.class.name == "Symbol"
208
+ if reflector.is_a? Symbol
161
209
  m.reflector = RotorMachine::Factory.build_reflector(reflector_kind: reflector)
162
- elsif reflector.class.name == "RotorMachine::Reflector"
210
+ elsif reflector.is_a? RotorMachine::Reflector
163
211
  m.reflector = reflector
164
212
  else
213
+ raise ArgumentError, "#{reflector} is not a reflector or reflector kind symbol"
165
214
  end
166
215
  end
167
216
 
@@ -1,4 +1,4 @@
1
- module RotorMachine
1
+ module RotorMachine
2
2
  ##
3
3
  # The {RotorMachine::Machine} class serves as the entrypoint and orchestrator
4
4
  # for an Enigma machine.
@@ -43,7 +43,7 @@ module RotorMachine
43
43
  # On a physical Enigma machine, the electrical signal from a keypress is
44
44
  # routed through the plugboard, then through each of the rotors in sequence
45
45
  # from left to right. The signal then passes through the reflector (where it
46
- # is transposed again), then back through the rotors in reverse order, and
46
+ # is transposed again), then back through the rotors in reverse order, and
47
47
  # finally back through the plugboard a second time before being displayed on
48
48
  # the light grid and/or printer.
49
49
  #
@@ -59,7 +59,7 @@ module RotorMachine
59
59
  # to help break the Enigma's encryption in World War II.
60
60
  #
61
61
  # == Usage
62
- #
62
+ #
63
63
  # To use the RotorMachine Enigma machine, you need to perform the following
64
64
  # steps:
65
65
  #
@@ -73,12 +73,24 @@ module RotorMachine
73
73
  # method to encode/decode, and {#set_rotors} to reset the machine state.
74
74
  #
75
75
  # The {#default_machine} and {#empty_machine} class methods are shortcut
76
- # factory methods whcih set up, respectively, a fully configured machine
76
+ # factory methods whcih set up, respectively, a fully configured machine
77
77
  # with a default set of rotors and reflector, and an empty machine with
78
78
  # no rotors or reflector.
79
79
  class Machine
80
80
  attr_accessor :rotors, :reflector, :plugboard
81
81
 
82
+ ##
83
+ # Initialize a RotorMachine object.
84
+ #
85
+ # This object won't be usable until you add rotors, a reflector and a
86
+ # plugboard. Using the {#default_machine} and {#empty_machine} helper class
87
+ # methods is the preferred way to initialize functioning machines.
88
+ def initialize()
89
+ @rotors = []
90
+ @reflector = nil
91
+ @plugboard = nil
92
+ end
93
+
82
94
  ##
83
95
  # Generates a default-configuration RotorMachine, with the following
84
96
  # state:
@@ -87,13 +99,12 @@ module RotorMachine
87
99
  # step at a time
88
100
  # - Reflector A
89
101
  # - An empty plugboard with no connections
102
+ #
103
+ # This method is just a proxy for the equivalently-named factory method in the
104
+ # {RotorMachine::Factory} class, and is maintained here for backward
105
+ # compatibility.
90
106
  def self.default_machine
91
- m = RotorMachine::Factory.build_machine(
92
- rotors: [:ROTOR_I, :ROTOR_II, :ROTOR_III],
93
- reflector: RotorMachine::Factory::build_reflector(reflector_kind: :REFLECTOR_A)
94
- )
95
- m.set_rotors("AAA")
96
- return m
107
+ RotorMachine::Factory.default_machine
97
108
  end
98
109
 
99
110
  ##
@@ -106,20 +117,12 @@ module RotorMachine
106
117
  #
107
118
  # A RotorMachine in this state will raise an {ArgumentError} until you
108
119
  # outfit it with at least one rotor and a reflector.
109
- def self.empty_machine
110
- RotorMachine::Factory.build_machine()
111
- end
112
-
113
- ##
114
- # Initialize a RotorMachine object.
115
120
  #
116
- # This object won't be usable until you add rotors, a reflector and a
117
- # plugboard. Using the {#default_machine} and {#empty_machine} helper class
118
- # methods is the preferred way to initialize functioning machines.
119
- def initialize()
120
- @rotors = []
121
- @reflector = nil
122
- @plugboard = nil
121
+ # This method is just a proxy for the equivalently-named factory method in the
122
+ # {RotorMachine::Factory} class, and is maintained here for backward
123
+ # compatibility.
124
+ def self.empty_machine
125
+ RotorMachine::Factory.empty_machine()
123
126
  end
124
127
 
125
128
  ##
@@ -140,8 +143,8 @@ module RotorMachine
140
143
  end
141
144
 
142
145
  ##
143
- # Coordinate the stepping of the set of rotors after a character is
144
- # enciphered.
146
+ # Coordinate the stepping of the set of rotors after a character is
147
+ # enciphered.
145
148
  def step_rotors
146
149
  @rotors.reverse.each do |rotor|
147
150
  rotor.step
@@ -156,7 +159,7 @@ module RotorMachine
156
159
  # This is a helper method to avoid having to manipulate the rotor
157
160
  # positions individually. Starting with the leftmost rotor, each
158
161
  # character from this string is used to set the position of one
159
- # rotor.
162
+ # rotor.
160
163
  #
161
164
  # If the string is longer than the number of rotors, the extra
162
165
  # values (to the right) are ignored. If it's shorter, the values of
@@ -185,7 +188,7 @@ module RotorMachine
185
188
  end
186
189
 
187
190
  ##
188
- # Encipher a single character.
191
+ # Encipher a single character.
189
192
  #
190
193
  # Used by {#encipher} to walk a single character of text through the
191
194
  # signal path of all components of the machine.
@@ -215,5 +218,174 @@ module RotorMachine
215
218
  end
216
219
  ec
217
220
  end
221
+
222
+ ##
223
+ # Create a Ruby hash containing a snapshot of the current machine state.
224
+ #
225
+ # The hash returned by this method contains enough information to capture
226
+ # the current internal state of the machine. Although you can invoke it
227
+ # directly if you want to, it is primarily intended to be accessed via
228
+ # the {#save_machine_state_to} and {#load_machine_state_from} methods,
229
+ # which save and load machine state to YAML files.
230
+ #
231
+ # @return [Hash] A Hash representing the internal state of the machine.
232
+ def machine_state
233
+ machine_state = {}
234
+ machine_state[:serialization_version] = RotorMachine::VERSION_DATA[0]
235
+
236
+ machine_state[:rotors] = []
237
+ self.rotors.each do |r|
238
+ rstate = {
239
+ kind: r.rotor_kind_name,
240
+ position: r.position,
241
+ step_size: r.step_size
242
+ }
243
+ if r.rotor_kind_name == :CUSTOM
244
+ rstate[:letters] = r.rotor_kind
245
+ end
246
+
247
+ machine_state[:rotors] << rstate
248
+ end
249
+ machine_state[:reflector] = {
250
+ kind: self.reflector.reflector_kind_name,
251
+ position: self.reflector.position
252
+ }
253
+ if (self.reflector.reflector_kind_name == :CUSTOM)
254
+ machine_state[:reflector][:letters] = self.reflector.letters
255
+ end
256
+
257
+ machine_state[:plugboard] = {
258
+ connections: self.plugboard.connections.clone
259
+ }
260
+ return machine_state
261
+ end
262
+
263
+ ##
264
+ # Write the internal machine state to a YAML file.
265
+ #
266
+ # The generated YAML file can be loaded using the #{load_machine_state_from}
267
+ # method to restore a saved machine state.
268
+ #
269
+ # @param filepath [String] The path to the YAML file to which the machine
270
+ # state should be saved.
271
+ # @return [Boolean] True if the save operation completed successfully, false
272
+ # if an error was raised.
273
+ def save_machine_state_to(filepath)
274
+ begin
275
+ File.open(filepath, "w") do |f|
276
+ f.puts machine_state.to_yaml
277
+ end
278
+ return true
279
+ rescue
280
+ return false
281
+ end
282
+ end
283
+
284
+ ##
285
+ # Read the internal machine state from a YAML file.
286
+ #
287
+ # The YAML file can be created using the #{save_machine_state_to} method to
288
+ # save the machine state of an existing {RotorMachine::Machine} object.
289
+ #
290
+ # The internal state is captured as is, so if you save the state from a machine
291
+ # that's not validly configured (no rotors, no reflector, etc.), the
292
+ # reconstituted machine will also have an invalid state.
293
+ #
294
+ # @param filepath [String] The path to the YAML file to which the machine
295
+ # state should be saved.
296
+ def load_machine_state_from(filepath)
297
+ raise ArgumentError, "File path \"#{filepath}\" not found!" unless File.exist?(filepath)
298
+ c = YAML.load(File.open(filepath))
299
+ self.set_machine_config_from(c)
300
+ return true
301
+ end
302
+
303
+ ##
304
+ # Create a new {RotorMachine::Machine} from a YAML configuration file.
305
+ #
306
+ # This class method is a one-step shortcut for creating an empty {RotorMachine::Machine}
307
+ # and then loading its machine state.
308
+ #
309
+ # @param config [Hash] A configuration hash for the new machine, such as a config
310
+ # hash generated by {#machine_state}.
311
+ # @return [RotorMachine::Machine] A new {RotorMachine::Machine} created from the
312
+ # supplied config hash.
313
+ def self.from_yaml(config)
314
+ unless config.keys.include?(:serialization_version)
315
+ raise ArgumentError, "Serialization Data Version Mismatch"
316
+ end
317
+ unless config[:serialization_version].is_a?(Numeric)
318
+ raise ArgumentError, "Serialization Data Version Mismatch"
319
+ end
320
+ if (config[:serialization_version] > RotorMachine::VERSION_DATA[0]) || (config[:serialization_version] < 1)
321
+ raise ArgumentError, "Serialization Data Version Mismatch"
322
+ end
323
+
324
+ m = self.empty_machine
325
+ m.set_machine_config_from(config)
326
+ return m
327
+ end
328
+
329
+ ##
330
+ # Set the state of the machine based on values in a config hash.
331
+ #
332
+ # Any config hash (such as that generated by {#machine_state}) can be provided
333
+ # as an argument, but this method is primarily intended to be accessed by the
334
+ # {#from_yaml} and {#load_config_state_from} methods to deserialize a machine
335
+ # state hash.
336
+ #
337
+ # @param config [Hash] The configuration hash describing the state of the
338
+ # {RotorMachine::Machine}.
339
+ # @return [RotorMachine::Machine] The {RotorMachine::Machine} which was just
340
+ # configured. def set_machine_config_from(config)
341
+ def set_machine_config_from(config)
342
+ @rotors = []
343
+ @reflector = nil
344
+ @plugboard = RotorMachine::Plugboard.new()
345
+
346
+ # Create rotors
347
+ config[:rotors].each do |rs|
348
+ if rs[:kind] == :CUSTOM
349
+ r = RotorMachine::Rotor.new(rs[:letters], rs[:position], rs[:step_size])
350
+ else
351
+ letters = RotorMachine::Rotor.const_get(rs[:kind])
352
+ r = RotorMachine::Rotor.new(letters, rs[:position], rs[:step_size])
353
+ end
354
+ @rotors << r
355
+ end
356
+
357
+ # Create reflector
358
+ if config[:reflector][:kind] == :CUSTOM
359
+ letters = config[:reflector][:letters]
360
+ else
361
+ letters = RotorMachine::Reflector.const_get(config[:reflector][:kind])
362
+ end
363
+ @reflector = RotorMachine::Reflector.new(letters, config[:reflector][:position])
364
+
365
+ # Plugboard mappings
366
+ config[:plugboard][:connections].keys.each do |l|
367
+ unless @plugboard.connected?(l)
368
+ @plugboard.connect(l, config[:plugboard][:connections][l])
369
+ end
370
+ end
371
+
372
+ return self
373
+ end
374
+
375
+ ##
376
+ # Compare another {RotorMachine::Machine} instance to this one.
377
+ #
378
+ # Returns true if the provided {RotorMachine::Machine} has the same
379
+ # configuration as this one, and false otherwise.
380
+ #
381
+ # @param another_machine [RotorMachine::Machine] The Machine to compare to
382
+ # this one.
383
+ # @return [Boolean] True if the machines have identical configuration, false
384
+ # otherwise.
385
+ def ==(another_machine)
386
+ @rotors == another_machine.rotors &&
387
+ @reflector == another_machine.reflector &&
388
+ @plugboard == another_machine.plugboard
389
+ end
218
390
  end
219
391
  end
@@ -16,8 +16,9 @@ module RotorMachine
16
16
  # from B to A.
17
17
  # - A letter cannot be connected to itself.
18
18
  class Plugboard
19
+ attr_reader :connections
19
20
 
20
- ##
21
+ ##
21
22
  # Create a new, empty Plugboard object.
22
23
  #
23
24
  # By default, no letters are connected in the plugboard, and all input
@@ -89,12 +90,25 @@ module RotorMachine
89
90
  @connections.keys.include?(letter.upcase)
90
91
  end
91
92
 
92
- ##
93
+ ##
93
94
  # Produce a human-readable representation of the #{Plugboard}'s state.
94
95
  #
95
96
  # @return [String] A description of the current state.
96
97
  def to_s
97
98
  "a RotorMachine::Plugboard with connections: #{@connections.to_s}"
98
99
  end
100
+
101
+ ##
102
+ # Compare this {RotorMachine::Plugboard} to another one.
103
+ #
104
+ # Returns True if the configuration of the supplied {RotorMachine::Plugboard}
105
+ # matches this one, false otherwise.
106
+ #
107
+ # @param another_plugboard [RotorMachine::Plugboard] The Plugboard to compare to
108
+ # this one.
109
+ # @return [Boolean] True if the configurations match, false otherwise.
110
+ def ==(another_plugboard)
111
+ @connections == another_plugboard.connections
112
+ end
99
113
  end
100
114
  end
@@ -58,6 +58,8 @@ module RotorMachine
58
58
  # Because the reflector does not rotate, this is essentially just
59
59
  # an additional permutation factor for the encipherment.
60
60
  def initialize(selected_reflector, start_position = 0)
61
+ raise ArgumentError, "Initialization string contains duplicate letters" unless selected_reflector.is_uniq?
62
+
61
63
  @letters = selected_reflector.chars.freeze
62
64
  @alphabet = ALPHABET.chars.freeze
63
65
  @position = start_position
@@ -139,5 +141,19 @@ module RotorMachine
139
141
  def to_s
140
142
  "a RotorMachine::Reflector of type '#{self.reflector_kind_name.to_s}'"
141
143
  end
144
+
145
+ ##
146
+ # Compare this {RotorMachine::Reflector} to another one.
147
+ #
148
+ # Returns True if the configuration of the supplied {RotorMachine::Reflector}
149
+ # matches this one, false otherwise.
150
+ #
151
+ # @param another_reflector [RotorMachine::Reflector] The Reflector to compare
152
+ # to this one.
153
+ # @return [Boolean] True if the configurations match, false otherwise.
154
+ def ==(another_reflector)
155
+ self.letters == another_reflector.letters &&
156
+ self.position == another_reflector.position
157
+ end
142
158
  end
143
159
  end
@@ -29,6 +29,8 @@ module RotorMachine
29
29
  # {Rotor} based on either a numeric position or a letter position.
30
30
  attr_reader :position
31
31
 
32
+ attr_reader :letters
33
+
32
34
  ##
33
35
  # Get or set the `step_size` - the number of positions the rotor should
34
36
  # advance every time it's stepped.
@@ -37,7 +39,7 @@ module RotorMachine
37
39
  ##
38
40
  # Provides the configuration of the German IC Enigma {Rotor}.
39
41
  ROTOR_IC = "DMTWSILRUYQNKFEJCAZBPGXOHV".freeze
40
-
42
+
41
43
  ##
42
44
  # Provides the configuration of the German IIC Enigma {Rotor}.
43
45
  ROTOR_IIC = "HQZGPJTMOBLNCIFDYAWVEUSRKX".freeze
@@ -84,6 +86,7 @@ module RotorMachine
84
86
  # @param step_size [Integer] The number of positions to step the rotor
85
87
  # each time it is advanced. Defaults to 1.
86
88
  def initialize(rotor, start_on=0, step_size=1)
89
+ raise ArgumentError, "Initialization string contains duplicate letters" unless rotor.is_uniq?
87
90
  @letters = rotor.chars.freeze
88
91
  self.position = start_on
89
92
  @step_size = step_size
@@ -197,5 +200,20 @@ module RotorMachine
197
200
  def to_s
198
201
  return "a RotorMachine::Rotor of type '#{self.rotor_kind_name}', position=#{self.position} (#{self.current_letter}), step_size=#{@step_size}"
199
202
  end
203
+
204
+ ##
205
+ # Compare this {RotorMachine::Rotor} to another one.
206
+ #
207
+ # Returns True if the configuration of the supplied {RotorMachine::Rotor} matches
208
+ # this one, false otherwise.
209
+ #
210
+ # @param another_rotor [RotorMachine::Rotor] The Rotor to compare to this one.
211
+ # @return [Boolean] True if the configurations match, false otherwise.
212
+ def ==(another_rotor)
213
+ @letters == another_rotor.letters &&
214
+ position == another_rotor.position &&
215
+ step_size == another_rotor.step_size
216
+ end
217
+
200
218
  end
201
219
  end
@@ -4,6 +4,16 @@
4
4
  # @author Tammy Cravit <tammycravit@me.com>
5
5
 
6
6
  class String
7
+
8
+ ##
9
+ # Detect if a string has any duplicated characters
10
+ #
11
+ # @return True if the string has no duplicated characters, false otherwise.
12
+ def is_uniq?
13
+ self.chars.uniq.length == self.chars.length
14
+ end
15
+ alias :uniq? :is_uniq?
16
+
7
17
  ##
8
18
  # Break a string into blocks of a certain number of characters.
9
19
  #
@@ -1,4 +1,4 @@
1
1
  module RotorMachine
2
- VERSION_DATA = [1, 0, 14]
2
+ VERSION_DATA = [1, 1, 1]
3
3
  VERSION = VERSION_DATA.join(".")
4
4
  end
@@ -8,8 +8,6 @@ Gem::Specification.new do |spec|
8
8
  spec.version = RotorMachine::VERSION
9
9
  spec.authors = ['Tammy Cravit']
10
10
  spec.email = ['tammycravit@me.com']
11
- spec.cert_chain = ['certs/tammycravit.pem']
12
- spec.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/
13
11
 
14
12
  spec.summary = %q{Simple Enigma-like rotor machine in Ruby}
15
13
  spec.homepage = 'https://github.com/tammycravit/rotor_machine'
@@ -22,15 +20,17 @@ Gem::Specification.new do |spec|
22
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
21
  spec.require_paths = ['lib']
24
22
 
25
- spec.add_dependency 'tcravit_ruby_lib'
23
+ spec.add_dependency 'tcravit_ruby_lib', '~> 0.2'
26
24
 
27
25
  spec.add_development_dependency 'pry', '~> 0.11'
26
+ spec.add_development_dependency 'pry-byebug', '~> 3.6'
28
27
  spec.add_development_dependency 'bundler', '~> 1.16'
29
28
  spec.add_development_dependency 'rake', '~> 10.0'
30
29
  spec.add_development_dependency 'rspec', '~> 3.0'
31
30
 
32
- spec.add_development_dependency 'guard'
33
- spec.add_development_dependency 'guard-rspec'
34
- spec.add_development_dependency 'guard-bundler'
35
- spec.add_development_dependency 'simplecov'
31
+ spec.add_development_dependency 'guard', '~> 2.14'
32
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
33
+ spec.add_development_dependency 'guard-bundler', '~> 2.1'
34
+ spec.add_development_dependency 'simplecov', '~> 0.15'
35
+ spec.add_development_dependency 'simplecov-erb', '~> 0.1'
36
36
  end
metadata CHANGED
@@ -1,55 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rotor_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.14
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tammy Cravit
8
8
  autorequire:
9
9
  bindir: exe
10
- cert_chain:
11
- - |
12
- -----BEGIN CERTIFICATE-----
13
- MIIEODCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBh0YW1t
14
- eWNyYXZpdC9EQz1tZS9EQz1jb20wHhcNMTgwMjE5MjMxNDQzWhcNMTkwMjE5MjMx
15
- NDQzWjAjMSEwHwYDVQQDDBh0YW1teWNyYXZpdC9EQz1tZS9EQz1jb20wggGiMA0G
16
- CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDDUddItdpGGMBoBfJZ2LWlXnfwEJWx
17
- iOc478enSEQFOLXj3nVuTUhyac6MQFH6nB8CkNZt7MSokuWdQ7H//1Ajq+jeCwUm
18
- WpjHF2BIL3WK7n8aAMH00p4gMAI4R8JnRjotmhUTIJCXtkIXoDTk1PGRzkH29q8+
19
- dByUGmkAoX+iHqNRLbgiywLlpVapRT5B1nE+K8oETb0TilCfdvOh+91dM1LX/z69
20
- uSOQOFoZSgnVNP/LTYaqDixdeEaDdslPRO1l8JSPdAzl1sn+YaeJwQbBWfbi+sGs
21
- MB53CkLGDsz5MsrPx8b0iBNM/xFSEmXE+du3vCSAZktjNR7kNuFGbSOQW4SQkld6
22
- mvw/Gi3TMmlgw2bELiXyUvEkHdkonFKMv9Rs2eq6Opw880YXl52/AcVi0dcsMZ3t
23
- qp70xuUgsDF7zNdSfIgQzBX+GsmIIgbRQKUyGuXMnqUJlPm4fPnl5i3mIyZnqKyx
24
- gg+2NNhKneabQF8wItZWdTGOVu87YFUOsosCAwEAAaN3MHUwCQYDVR0TBAIwADAL
25
- BgNVHQ8EBAMCBLAwHQYDVR0OBBYEFMSFUfz/8w+SLDZIFy54jEKUPq4IMB0GA1Ud
26
- EQQWMBSBEnRhbW15Y3Jhdml0QG1lLmNvbTAdBgNVHRIEFjAUgRJ0YW1teWNyYXZp
27
- dEBtZS5jb20wDQYJKoZIhvcNAQELBQADggGBAC/hS37ZCB/MYxt6gE9i5qvjdY5j
28
- qPiiQ7i5Yf2Gx6Jbe/wxiW1A3QcMdRvUSfIdC3XP3rYQf0AiyaQmbxhRn5e0LkYd
29
- riChjHZxLQG3CKj+7YiUijIv0mgaw/lA0pEhMxIb/xY03Jwh64cg2FZrd/5wWLh2
30
- QpyGVAktJp3rQolYO0fXbqRt40lg2+h2UWmaFvj++sFoCWdZzaopJZ3CS96IgUt+
31
- sqm+r9HvzygOChJyLAjM8OwabZ4e2yRR2ZLiRxvHBL4FGf7hg6Y0YAvwvyRJw/7b
32
- x6WTe0KO4pSZD02hl1A4gblx72eDvRwYkWO+dT1j9R+Wvrp/puwnzrLdThLwTsWQ
33
- YGgdQBodP4Wqsew3nfbNJOKkqOnry4lWJugso3w2fe0nUbrWuaC3++J9Eazm++n/
34
- F9wFDQvW5Nv6grw3Unc9miwN6NHA5kEjKzDDSXzWKSzAWbqzlMp/FxD+zP7NuibT
35
- pjZmBE7TzW3JK1L4mE7lBh9bwUC5WoyMBDPT7A==
36
- -----END CERTIFICATE-----
37
- date: 2018-02-19 00:00:00.000000000 Z
10
+ cert_chain: []
11
+ date: 2018-06-22 00:00:00.000000000 Z
38
12
  dependencies:
39
13
  - !ruby/object:Gem::Dependency
40
14
  name: tcravit_ruby_lib
41
15
  requirement: !ruby/object:Gem::Requirement
42
16
  requirements:
43
- - - ">="
17
+ - - "~>"
44
18
  - !ruby/object:Gem::Version
45
- version: '0'
19
+ version: '0.2'
46
20
  type: :runtime
47
21
  prerelease: false
48
22
  version_requirements: !ruby/object:Gem::Requirement
49
23
  requirements:
50
- - - ">="
24
+ - - "~>"
51
25
  - !ruby/object:Gem::Version
52
- version: '0'
26
+ version: '0.2'
53
27
  - !ruby/object:Gem::Dependency
54
28
  name: pry
55
29
  requirement: !ruby/object:Gem::Requirement
@@ -64,6 +38,20 @@ dependencies:
64
38
  - - "~>"
65
39
  - !ruby/object:Gem::Version
66
40
  version: '0.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
67
55
  - !ruby/object:Gem::Dependency
68
56
  name: bundler
69
57
  requirement: !ruby/object:Gem::Requirement
@@ -110,58 +98,72 @@ dependencies:
110
98
  name: guard
111
99
  requirement: !ruby/object:Gem::Requirement
112
100
  requirements:
113
- - - ">="
101
+ - - "~>"
114
102
  - !ruby/object:Gem::Version
115
- version: '0'
103
+ version: '2.14'
116
104
  type: :development
117
105
  prerelease: false
118
106
  version_requirements: !ruby/object:Gem::Requirement
119
107
  requirements:
120
- - - ">="
108
+ - - "~>"
121
109
  - !ruby/object:Gem::Version
122
- version: '0'
110
+ version: '2.14'
123
111
  - !ruby/object:Gem::Dependency
124
112
  name: guard-rspec
125
113
  requirement: !ruby/object:Gem::Requirement
126
114
  requirements:
127
- - - ">="
115
+ - - "~>"
128
116
  - !ruby/object:Gem::Version
129
- version: '0'
117
+ version: '4.7'
130
118
  type: :development
131
119
  prerelease: false
132
120
  version_requirements: !ruby/object:Gem::Requirement
133
121
  requirements:
134
- - - ">="
122
+ - - "~>"
135
123
  - !ruby/object:Gem::Version
136
- version: '0'
124
+ version: '4.7'
137
125
  - !ruby/object:Gem::Dependency
138
126
  name: guard-bundler
139
127
  requirement: !ruby/object:Gem::Requirement
140
128
  requirements:
141
- - - ">="
129
+ - - "~>"
142
130
  - !ruby/object:Gem::Version
143
- version: '0'
131
+ version: '2.1'
144
132
  type: :development
145
133
  prerelease: false
146
134
  version_requirements: !ruby/object:Gem::Requirement
147
135
  requirements:
148
- - - ">="
136
+ - - "~>"
149
137
  - !ruby/object:Gem::Version
150
- version: '0'
138
+ version: '2.1'
151
139
  - !ruby/object:Gem::Dependency
152
140
  name: simplecov
153
141
  requirement: !ruby/object:Gem::Requirement
154
142
  requirements:
155
- - - ">="
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.15'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.15'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov-erb
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
156
158
  - !ruby/object:Gem::Version
157
- version: '0'
159
+ version: '0.1'
158
160
  type: :development
159
161
  prerelease: false
160
162
  version_requirements: !ruby/object:Gem::Requirement
161
163
  requirements:
162
- - - ">="
164
+ - - "~>"
163
165
  - !ruby/object:Gem::Version
164
- version: '0'
166
+ version: '0.1'
165
167
  description:
166
168
  email:
167
169
  - tammycravit@me.com
@@ -182,8 +184,8 @@ files:
182
184
  - README.md
183
185
  - Rakefile
184
186
  - bin/console
187
+ - bin/resolve_coverage.pl
185
188
  - bin/setup
186
- - certs/tammycravit.pem
187
189
  - exe/rotor_machine
188
190
  - images/Bundesarchiv_Enigma.jpg
189
191
  - images/File:Enigma_wiring_kleur.png
@@ -216,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
218
  version: '0'
217
219
  requirements: []
218
220
  rubyforge_project:
219
- rubygems_version: 2.7.5
221
+ rubygems_version: 2.7.7
220
222
  signing_key:
221
223
  specification_version: 4
222
224
  summary: Simple Enigma-like rotor machine in Ruby
checksums.yaml.gz.sig DELETED
@@ -1,2 +0,0 @@
1
- *�Qf�ڲ�g�Eq
2
- 7 ����v(X�\�2z>����,�wg
data.tar.gz.sig DELETED
@@ -1,2 +0,0 @@
1
- �?S�:��� 3�
2
- �0&������cr�5L�f����-?>��"��ao��� i���rb8����p���O����#ޔu�#�\������oP��t�x�0 bpɠ�?���,u7��ޠ>L14.���+��
@@ -1,25 +0,0 @@
1
- -----BEGIN CERTIFICATE-----
2
- MIIEODCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBh0YW1t
3
- eWNyYXZpdC9EQz1tZS9EQz1jb20wHhcNMTgwMjE5MjMxNDQzWhcNMTkwMjE5MjMx
4
- NDQzWjAjMSEwHwYDVQQDDBh0YW1teWNyYXZpdC9EQz1tZS9EQz1jb20wggGiMA0G
5
- CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDDUddItdpGGMBoBfJZ2LWlXnfwEJWx
6
- iOc478enSEQFOLXj3nVuTUhyac6MQFH6nB8CkNZt7MSokuWdQ7H//1Ajq+jeCwUm
7
- WpjHF2BIL3WK7n8aAMH00p4gMAI4R8JnRjotmhUTIJCXtkIXoDTk1PGRzkH29q8+
8
- dByUGmkAoX+iHqNRLbgiywLlpVapRT5B1nE+K8oETb0TilCfdvOh+91dM1LX/z69
9
- uSOQOFoZSgnVNP/LTYaqDixdeEaDdslPRO1l8JSPdAzl1sn+YaeJwQbBWfbi+sGs
10
- MB53CkLGDsz5MsrPx8b0iBNM/xFSEmXE+du3vCSAZktjNR7kNuFGbSOQW4SQkld6
11
- mvw/Gi3TMmlgw2bELiXyUvEkHdkonFKMv9Rs2eq6Opw880YXl52/AcVi0dcsMZ3t
12
- qp70xuUgsDF7zNdSfIgQzBX+GsmIIgbRQKUyGuXMnqUJlPm4fPnl5i3mIyZnqKyx
13
- gg+2NNhKneabQF8wItZWdTGOVu87YFUOsosCAwEAAaN3MHUwCQYDVR0TBAIwADAL
14
- BgNVHQ8EBAMCBLAwHQYDVR0OBBYEFMSFUfz/8w+SLDZIFy54jEKUPq4IMB0GA1Ud
15
- EQQWMBSBEnRhbW15Y3Jhdml0QG1lLmNvbTAdBgNVHRIEFjAUgRJ0YW1teWNyYXZp
16
- dEBtZS5jb20wDQYJKoZIhvcNAQELBQADggGBAC/hS37ZCB/MYxt6gE9i5qvjdY5j
17
- qPiiQ7i5Yf2Gx6Jbe/wxiW1A3QcMdRvUSfIdC3XP3rYQf0AiyaQmbxhRn5e0LkYd
18
- riChjHZxLQG3CKj+7YiUijIv0mgaw/lA0pEhMxIb/xY03Jwh64cg2FZrd/5wWLh2
19
- QpyGVAktJp3rQolYO0fXbqRt40lg2+h2UWmaFvj++sFoCWdZzaopJZ3CS96IgUt+
20
- sqm+r9HvzygOChJyLAjM8OwabZ4e2yRR2ZLiRxvHBL4FGf7hg6Y0YAvwvyRJw/7b
21
- x6WTe0KO4pSZD02hl1A4gblx72eDvRwYkWO+dT1j9R+Wvrp/puwnzrLdThLwTsWQ
22
- YGgdQBodP4Wqsew3nfbNJOKkqOnry4lWJugso3w2fe0nUbrWuaC3++J9Eazm++n/
23
- F9wFDQvW5Nv6grw3Unc9miwN6NHA5kEjKzDDSXzWKSzAWbqzlMp/FxD+zP7NuibT
24
- pjZmBE7TzW3JK1L4mE7lBh9bwUC5WoyMBDPT7A==
25
- -----END CERTIFICATE-----
metadata.gz.sig DELETED
Binary file