sixword 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,33 @@
1
1
  module Sixword
2
+
3
+ # The Sixword::CLI class implements all of the complex processing needed for
4
+ # the sixword Command Line Interface.
2
5
  class CLI
6
+
7
+ # Exception for certain input validation errors
3
8
  class CLIError < StandardError; end
4
9
 
5
- attr_reader :filename, :options, :stream, :mode
10
+ # @return [String] Input filename
11
+ attr_reader :filename
12
+
13
+ # @return [Hash] Options hash
14
+ attr_reader :options
15
+
16
+ # @return [File, IO] Stream opened from #filename
17
+ attr_reader :stream
18
+
19
+ # @return [:encode, :decode]
20
+ attr_reader :mode
6
21
 
22
+ # Create a Sixword CLI to operate on filename with options
23
+ #
24
+ # @param filename [String] Input file name (or '-' for stdin)
25
+ # @param options [Hash]
26
+ #
27
+ # @option options [:encode, :decode] :mode (:encode)
28
+ # @option options [Boolean] :pad (false)
29
+ # @option options [String] :hex_style
30
+ #
7
31
  def initialize(filename, options)
8
32
  @filename = filename
9
33
  @options = {mode: :encode, pad: false}.merge(options)
@@ -24,18 +48,25 @@ module Sixword
24
48
  end
25
49
  end
26
50
 
51
+ # Return the value of the :pad option.
52
+ # @return [Boolean]
27
53
  def pad?
28
54
  options.fetch(:pad)
29
55
  end
30
56
 
57
+ # Return true if we are in encoding mode, false otherwise (decoding).
58
+ # @return [Boolean]
31
59
  def encoding?
32
60
  mode == :encode
33
61
  end
34
62
 
63
+ # Return the value of the :hex_style option.
64
+ # @return [String, nil]
35
65
  def hex_style
36
66
  options[:hex_style]
37
67
  end
38
68
 
69
+ # Format data as hex in various styles.
39
70
  def print_hex(data, chunk_index, cols=80)
40
71
  case hex_style
41
72
  when 'lower', 'lowercase'
@@ -59,6 +90,7 @@ module Sixword
59
90
  end
60
91
  end
61
92
 
93
+ # Run the encoding/decoding operation, printing the result to stdout.
62
94
  def run!
63
95
  if encoding?
64
96
  do_encode! do |encoded|
@@ -87,7 +119,6 @@ module Sixword
87
119
  raise ArgumentError.new("block is required")
88
120
  end
89
121
 
90
- arr = []
91
122
  read_input_by_6_words do |arr|
92
123
  yield Sixword.decode(arr, padding_ok: pad?)
93
124
  end
@@ -140,7 +171,7 @@ module Sixword
140
171
  while true
141
172
  buf = stream.read(block_size)
142
173
  if buf.nil?
143
- break #EOF
174
+ break # EOF
144
175
  end
145
176
 
146
177
  buf.scan(/\S+/) do |word|
@@ -1,12 +1,24 @@
1
1
  module Sixword
2
+ # Various hexadecimal string encoding and decoding functions
2
3
  module Hex
3
4
  HexValid = /\A[a-fA-F0-9]+\z/
4
5
  HexStrip = /[\s:.-]+/
5
6
 
7
+ # Return whether string is entirely hexadecimal.
8
+ # @param string [String]
9
+ # @return [Boolean]
10
+ # @see [HexValid]
11
+ #
6
12
  def self.valid_hex?(string)
7
13
  !!(string =~ HexValid)
8
14
  end
9
15
 
16
+ # Return whether single character string is one of the fill characters that
17
+ # are OK to strip from a hexadecimal string.
18
+ # @param char [String] String of length == 1
19
+ # @return [Boolean]
20
+ # @see [HexStrip]
21
+ #
10
22
  def self.strip_char?(char)
11
23
  unless char.length == 1
12
24
  raise ArgumentError.new("Must pass single character string")
@@ -14,22 +26,71 @@ module Sixword
14
26
  !!(char =~ HexStrip)
15
27
  end
16
28
 
29
+ # Encode a byte string as hexadecimal.
30
+ #
31
+ # @param bytes [String]
32
+ # @return [String] hexadecimal string
33
+ #
17
34
  def self.encode(bytes)
18
35
  bytes.unpack('H*').fetch(0)
19
36
  end
20
37
 
38
+ # Encode a byte string as hexadecimal, returning it in slices joined by a
39
+ # delimiter. This is useful for generating colon or space separated strings
40
+ # like those commonly used in fingerprints.
41
+ #
42
+ # @param bytes [String]
43
+ # @param slice [Integer]
44
+ # @param delimiter [String]
45
+ #
46
+ # @return [String]
47
+ #
48
+ # @example
49
+ # >> encode_slice("9T]B\xF0\x039\xFF", 2, ':')
50
+ # => "39:54:5d:42:f0:03:39:ff"
51
+ #
21
52
  def self.encode_slice(bytes, slice, delimiter)
22
53
  encode(bytes).each_char.each_slice(slice).map(&:join).join(delimiter)
23
54
  end
24
55
 
56
+ # Encode a byte string as a GPG style fingerprint: uppercase in slices of 4
57
+ # separated by spaces.
58
+ #
59
+ # @param bytes [String]
60
+ # @return [String]
61
+ #
62
+ # @example
63
+ # >> encode_fingerprint("9T]B\xF0\x039\xFF")
64
+ # => "3954 5D42 F003 39FF"
65
+ #
25
66
  def self.encode_fingerprint(bytes)
26
67
  encode_slice(bytes, 4, ' ').upcase
27
68
  end
28
69
 
70
+ # Encode a byte string in hex with colons: lowercase in slices of 2
71
+ # separated by colons.
72
+ #
73
+ # @param bytes [String]
74
+ # @return [String]
75
+ #
76
+ # @example
77
+ # >> encode_colons("9T]B\xF0\x039\xFF")
78
+ # => "39:54:5d:42:f0:03:39:ff"
79
+ #
80
+ #
29
81
  def self.encode_colons(bytes)
30
82
  encode_slice(bytes, 2, ':')
31
83
  end
32
84
 
85
+ # Decode a hexadecimal string to a byte string.
86
+ #
87
+ # @param hex_string [String]
88
+ # @param strip_chars [Boolean] Whether to accept and strip whitespace and
89
+ # other delimiters (see {HexStrip})
90
+ # @return [String]
91
+ #
92
+ # @raise ArgumentError on invalid hex input
93
+ #
33
94
  def self.decode(hex_string, strip_chars=true)
34
95
  if strip_chars
35
96
  hex_string = hex_string.gsub(HexStrip, '')
@@ -39,7 +100,7 @@ module Sixword
39
100
  raise ArgumentError.new("Invalid value for hex: #{hex_string.inspect}")
40
101
  end
41
102
 
42
- unless hex_string.length % 2 == 0
103
+ unless hex_string.length.even?
43
104
  raise ArgumentError.new("Odd length hex: #{hex_string.inspect}")
44
105
  end
45
106
 
@@ -1,5 +1,26 @@
1
1
  module Sixword
2
+
3
+ # The Lib module contains various internal utility functions. They are not
4
+ # really part of the public API and will probably not be useful to external
5
+ # callers.
2
6
  module Lib
7
+
8
+ # Encode an array of 8 bytes as an array of 6 words.
9
+ #
10
+ # @param byte_array [Array<Fixnum>] An array of length 8 containing
11
+ # integers in 0..255
12
+ #
13
+ # @return [Array<String>] An array of length 6 containing String words from
14
+ # {Sixword::WORDS}
15
+ #
16
+ # @example
17
+ # >> Sixword::Lib.encode_64_bits([0] * 8)
18
+ # => ["A", "A", "A", "A", "A", "A"]
19
+ #
20
+ # @example
21
+ # >> Sixword::Lib.encode_64_bits([0xff] * 8)
22
+ # => ["YOKE", "YOKE", "YOKE", "YOKE", "YOKE", "YEAR"]
23
+ #
3
24
  def self.encode_64_bits(byte_array)
4
25
  unless byte_array.length == 8
5
26
  raise ArgumentError.new("Must pass an 8-byte array")
@@ -23,6 +44,22 @@ module Sixword
23
44
  encoded
24
45
  end
25
46
 
47
+ # Decode an array of 6 words into a 64-bit integer (representing 8 bytes).
48
+ #
49
+ # @param word_array [Array<String>] A 6 element array of String words
50
+ # @param padding_ok [Boolean]
51
+ #
52
+ # @return [Array(Integer, Integer)] a 64-bit integer (the data) and the
53
+ # length of the byte array that it represents (will always be 8 unless
54
+ # padding_ok)
55
+ #
56
+ # @example
57
+ # >> Sixword::Lib.decode_6_words(%w{COAT ACHE A A A ACT6}, true)
58
+ # => [26729, 2]
59
+ #
60
+ # >> Sixword::Lib.decode_6_words(%w{ACRE ADEN INN SLID MAD PAP}, false)
61
+ # => [5217737340628397156, 8]
62
+ #
26
63
  def self.decode_6_words(word_array, padding_ok)
27
64
  unless word_array.length == 6
28
65
  raise ArgumentError.new("Must pass a six-word array")
@@ -68,7 +105,15 @@ module Sixword
68
105
  [int, 8 - padding]
69
106
  end
70
107
 
71
- # Extract the padding from a word, e.g. 'WORD3' => 'WORD', 3
108
+ # Extract the numeric padding from a word.
109
+ #
110
+ # @param word [String]
111
+ # @return [Array(String, Integer)] The String word, the Integer padding
112
+ #
113
+ # @example
114
+ # >> Sixword::Lib.extract_padding("WORD3")
115
+ # => ["WORD", 3]
116
+ #
72
117
  def self.extract_padding(word)
73
118
  unless word[-1] =~ /[1-7]/
74
119
  raise ArgumentError.new("Not a valid padded word: #{word.inspect}")
@@ -77,11 +122,34 @@ module Sixword
77
122
  return word[0...-1], Integer(word[-1])
78
123
  end
79
124
 
125
+ # Decode an array of 6 words into a String of bytes.
126
+ #
127
+ # @param word_array [Array<String>] A 6 element array of String words
128
+ # @param padding_ok [Boolean]
129
+ #
130
+ # @return [String]
131
+ #
132
+ # @see Sixword.decode_6_words
133
+ # @see Sixword.int_to_byte_array
134
+ #
135
+ # @example
136
+ # >> Lib.decode_6_words_to_bstring(%w{COAT ACHE A A A ACT6}, true)
137
+ # => "hi"
138
+ #
139
+ # >> Lib.decode_6_words_to_bstring(%w{ACRE ADEN INN SLID MAD PAP}, false)
140
+ # => "Hi world"
141
+ #
80
142
  def self.decode_6_words_to_bstring(word_array, padding_ok)
81
143
  int_to_byte_array(*decode_6_words(word_array, padding_ok)).
82
144
  map(&:chr).join
83
145
  end
84
146
 
147
+ # Given a word, return the 11 bits it represents as an integer (i.e. its
148
+ # index in the WORDS list).
149
+ #
150
+ # @param word [String]
151
+ # @return [Fixnum] An integer 0..2047
152
+ #
85
153
  def self.word_to_bits(word)
86
154
  word = word.upcase
87
155
  return WORDS_HASH.fetch(word)
@@ -94,6 +162,13 @@ module Sixword
94
162
  end
95
163
 
96
164
  # Compute two-bit parity on a byte array by summing each pair of bits.
165
+ # TODO: figure out which is faster
166
+ #
167
+ # @param byte_array [Array<Fixnum>]
168
+ # @return [Fixnum] An integer 0..3
169
+ #
170
+ # @see parity_int
171
+ #
97
172
  def self.parity_array(byte_array)
98
173
 
99
174
  # sum pairs of bits through the whole array
@@ -109,7 +184,15 @@ module Sixword
109
184
  parity & 0b11
110
185
  end
111
186
 
112
- # Compute parity in a different way. TODO: figure out which is faster
187
+ # Compute two-bit parity on a 64-bit integer representing an 8-byte array
188
+ # by summing each pair of bits.
189
+ # TODO: figure out which is faster
190
+ #
191
+ # @param int [Integer] A 64-bit integer representing 8 bytes
192
+ # @return [Fixnum] An integer 0..3
193
+ #
194
+ # @see parity_array
195
+ #
113
196
  def self.parity_int(int)
114
197
  parity = 0
115
198
  while int > 0
@@ -122,14 +205,14 @@ module Sixword
122
205
 
123
206
  # Given an array of bytes, pack them into a single Integer.
124
207
  #
125
- # For example:
208
+ # @example
126
209
  #
127
210
  # >> byte_array_to_int([1, 2])
128
211
  # => 258
129
212
  #
130
213
  # @param byte_array [Array<Fixnum>]
131
214
  #
132
- # @return Integer
215
+ # @return [Integer]
133
216
  #
134
217
  def self.byte_array_to_int(byte_array)
135
218
  int = 0
@@ -142,19 +225,19 @@ module Sixword
142
225
 
143
226
  # Given an Integer, unpack it into an array of bytes.
144
227
  #
145
- # For example:
146
- #
228
+ # @example
147
229
  # >> int_to_byte_array(258)
148
230
  # => [1, 2]
149
231
  #
232
+ # @example
150
233
  # >> int_to_byte_array(258, 3)
151
234
  # => [0, 1, 2]
152
235
  #
153
236
  # @param int [Integer]
154
- # @param length [Integer] (nil) Left zero padded size of byte array to
155
- # return. If not provided, no leading zeroes will be added.
237
+ # @param length [Integer] Left zero padded size of byte array to return. If
238
+ # not provided, no leading zeroes will be added.
156
239
  #
157
- # @return Array<Fixnum>
240
+ # @return [Array<Fixnum>]
158
241
  #
159
242
  def self.int_to_byte_array(int, length=nil)
160
243
  unless int >= 0
@@ -1,3 +1,4 @@
1
1
  module Sixword
2
- VERSION = '0.3.1'
2
+ # version string
3
+ VERSION = '0.3.2'
3
4
  end
@@ -264,6 +264,7 @@ module Sixword
264
264
  WORDS.freeze
265
265
  WORDS.each(&:freeze)
266
266
 
267
+ # A mapping from Word => Integer index in the word list
267
268
  WORDS_HASH = Hash[WORDS.each_with_index.to_a]
268
269
  WORDS_HASH.freeze
269
270
  end
@@ -28,8 +28,11 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  spec.add_development_dependency 'bundler', '~> 1.3'
31
+ spec.add_development_dependency 'pry'
31
32
  spec.add_development_dependency 'rake'
32
- spec.add_development_dependency 'rspec'
33
+ spec.add_development_dependency 'rspec', '~> 3.0'
34
+ spec.add_development_dependency 'rubocop', '~> 0'
35
+ spec.add_development_dependency 'yard'
33
36
 
34
37
  spec.required_ruby_version = '>= 1.9.3'
35
38
  end
@@ -1,7 +1,6 @@
1
1
  # coding: binary
2
- require_relative '../rspec_helper'
3
2
 
4
- describe Sixword::Hex do
3
+ RSpec.describe Sixword::Hex do
5
4
  TestCases = {
6
5
  "\x73\xe2\x16\xb5\x36\x3f\x23\x77" => [
7
6
  "73e216b5363f2377",
@@ -88,38 +87,38 @@ describe Sixword::Hex do
88
87
 
89
88
  it 'should decode and encode random hex strings correctly' do
90
89
  TestCases.each do |binary, hexes|
91
- lower, finger, colons = hexes
92
- Sixword::Hex.encode(binary).should == lower
93
- Sixword::Hex.decode(lower).should == binary
90
+ lower, _finger, _colons = hexes
91
+ expect(Sixword::Hex.encode(binary)).to eq(lower)
92
+ expect(Sixword::Hex.decode(lower)).to eq(binary)
94
93
  end
95
94
  end
96
95
 
97
96
  it 'should decode and encode random hex fingerprints correctly' do
98
97
  TestCases.each do |binary, hexes|
99
- lower, finger, colons = hexes
100
- Sixword::Hex.encode_fingerprint(binary).should == finger
101
- Sixword::Hex.decode(finger).should == binary
98
+ _lower, finger, _colons = hexes
99
+ expect(Sixword::Hex.encode_fingerprint(binary)).to eq(finger)
100
+ expect(Sixword::Hex.decode(finger)).to eq(binary)
102
101
  end
103
102
  end
104
103
 
105
104
  it 'should decode and encode random colon hexes correctly' do
106
105
  TestCases.each do |binary, hexes|
107
- lower, finger, colons = hexes
108
- Sixword::Hex.encode_colons(binary).should == colons
109
- Sixword::Hex.decode(colons).should == binary
106
+ _lower, _finger, colons = hexes
107
+ expect(Sixword::Hex.encode_colons(binary)).to eq(colons)
108
+ expect(Sixword::Hex.decode(colons)).to eq(binary)
110
109
  end
111
110
  end
112
111
 
113
112
  it 'should accept all valid hex characters' do
114
- Sixword::Hex.valid_hex?('abcdefABCDEF0123456789').should == true
113
+ expect(Sixword::Hex.valid_hex?('abcdefABCDEF0123456789')).to eq(true)
115
114
  end
116
115
 
117
116
  it 'should reject invalid hex characters' do
118
117
  ('g'..'z').each do |c|
119
- Sixword::Hex.valid_hex?(c).should == false
118
+ expect(Sixword::Hex.valid_hex?(c)).to eq(false)
120
119
  end
121
120
  ('G'..'Z').each do |c|
122
- Sixword::Hex.valid_hex?(c).should == false
121
+ expect(Sixword::Hex.valid_hex?(c)).to eq(false)
123
122
  end
124
123
  end
125
124
  end