rotor_machine 1.0.14 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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