sixword 0.3.1 → 0.3.2

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.
@@ -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