module Sixword::Lib

The Lib module contains various internal utility functions. They are not really part of the public API and will probably not be useful to external callers.

Public Class Methods

byte_array_to_int(byte_array) click to toggle source

Given an array of bytes, pack them into a single Integer.

@example

>> byte_array_to_int([1, 2])
=> 258

@param byte_array [Array<Fixnum>]

@return [Integer]

# File lib/sixword/lib.rb, line 218
def self.byte_array_to_int(byte_array)
  int = 0
  byte_array.each do |byte|
    int <<= 8
    int |= byte
  end
  int
end
decode_6_words(word_array, padding_ok) click to toggle source

Decode an array of 6 words into a 64-bit integer (representing 8 bytes).

@param word_array [Array<String>] A 6 element array of String words @param padding_ok [Boolean]

@return [Array(Integer, Integer)] a 64-bit integer (the data) and the length of the byte array that it represents (will always be 8 unless padding_ok)

@example

>> Sixword::Lib.decode_6_words(%w{COAT ACHE A A A ACT6}, true)
=> [26729, 2]

>> Sixword::Lib.decode_6_words(%w{ACRE ADEN INN SLID MAD PAP}, false)
=> [5217737340628397156, 8]
# File lib/sixword/lib.rb, line 63
def self.decode_6_words(word_array, padding_ok)
  unless word_array.length == 6
    raise ArgumentError.new("Must pass a six-word array")
  end

  bits_array = []

  padding = 0

  # extract padding, if any
  if padding_ok && word_array[-1][-1] =~ /[1-7]/
    word, padding = extract_padding(word_array[-1])
    word_array[-1] = word
  end

  bits_array = word_array.map {|w| word_to_bits(w) }

  bits_array.each do |bits|
    if bits >= 2048 || bits < 0
      raise RuntimeError.new("Somehow got bits of #{bits.inspect}")
    end
  end

  int = 0
  (0..4).each do |i|
    int <<= 11
    int += bits_array.fetch(i)
  end

  # slice out parity from last word
  parity = bits_array.fetch(5) & 0b11
  int <<= 9
  int += bits_array.fetch(5) >> 2

  # check parity
  unless parity_int(int) == parity
    raise InvalidParity.new("Parity bits do not match: " +
                            word_array.join(' ').inspect)
  end

  # omit padding bits, if any
  int >>= padding * 8

  [int, 8 - padding]
end
decode_6_words_to_bstring(word_array, padding_ok) click to toggle source

Decode an array of 6 words into a String of bytes.

@param word_array [Array<String>] A 6 element array of String words @param padding_ok [Boolean]

@return [String]

@see Sixword.decode_6_words @see Sixword.int_to_byte_array

@example

>> Lib.decode_6_words_to_bstring(%w{COAT ACHE A A A ACT6}, true)
=> "hi"

>> Lib.decode_6_words_to_bstring(%w{ACRE ADEN INN SLID MAD PAP}, false)
=> "Hi world"
# File lib/sixword/lib.rb, line 143
def self.decode_6_words_to_bstring(word_array, padding_ok)
  int_to_byte_array(*decode_6_words(word_array, padding_ok)).
    map(&:chr).join
end
encode_64_bits(byte_array) click to toggle source

Encode an array of 8 bytes as an array of 6 words.

@param byte_array [Array<Fixnum>] An array of length 8 containing

integers in 0..255

@return [Array<String>] An array of length 6 containing String words from

{Sixword::WORDS}

@example

>> Sixword::Lib.encode_64_bits([0] * 8)
=> ["A", "A", "A", "A", "A", "A"]

@example

>> Sixword::Lib.encode_64_bits([0xff] * 8)
=> ["YOKE", "YOKE", "YOKE", "YOKE", "YOKE", "YEAR"]
# File lib/sixword/lib.rb, line 24
def self.encode_64_bits(byte_array)
  unless byte_array.length == 8
    raise ArgumentError.new("Must pass an 8-byte array")
  end

  int = byte_array_to_int(byte_array)

  parity_bits = parity_int(int)

  encoded = Array.new(6)

  last_index = ((int & 511) << 2) | parity_bits
  encoded[5] = WORDS.fetch(last_index).dup
  int >>= 9

  4.downto(0) do |i|
    encoded[i] = WORDS.fetch(int & 2047).dup
    int >>= 11
  end

  encoded
end
extract_padding(word) click to toggle source

Extract the numeric padding from a word.

@param word [String] @return [Array(String, Integer)] The String word, the Integer padding

@example

>> Sixword::Lib.extract_padding("WORD3")
=> ["WORD", 3]
# File lib/sixword/lib.rb, line 118
def self.extract_padding(word)
  unless word[-1] =~ /[1-7]/
    raise ArgumentError.new("Not a valid padded word: #{word.inspect}")
  end

  return word[0...-1], Integer(word[-1])
end
int_to_byte_array(int, length=nil) click to toggle source

Given an Integer, unpack it into an array of bytes.

@example

>> int_to_byte_array(258)
=> [1, 2]

@example

>> int_to_byte_array(258, 3)
=> [0, 1, 2]

@param int [Integer] @param length [Integer] Left zero padded size of byte array to return. If

not provided, no leading zeroes will be added.

@return [Array<Fixnum>]

# File lib/sixword/lib.rb, line 243
def self.int_to_byte_array(int, length=nil)
  unless int >= 0
    raise ArgumentError.new("Not sure what to do with negative numbers")
  end

  arr = []

  while int > 0
    arr << (int & 255)
    int >>= 8
  end

  # pad to appropriate length with leading zeroes
  if length
    raise ArgumentError.new("Cannot pad to length < 0") if length < 0

    while arr.length < length
      arr << 0
    end
  end

  arr.reverse!

  arr
end
parity_array(byte_array) click to toggle source

Compute two-bit parity on a byte array by summing each pair of bits. TODO: figure out which is faster

@param byte_array [Array<Fixnum>] @return [Fixnum] An integer 0..3

@see parity_int

# File lib/sixword/lib.rb, line 173
def self.parity_array(byte_array)

  # sum pairs of bits through the whole array
  parity = 0
  byte_array.each do |byte|
    while byte > 0
      parity += byte & 0b11
      byte >>= 2
    end
  end

  # return the least significant two bits
  parity & 0b11
end
parity_int(int) click to toggle source

Compute two-bit parity on a 64-bit integer representing an 8-byte array by summing each pair of bits. TODO: figure out which is faster

@param int [Integer] A 64-bit integer representing 8 bytes @return [Fixnum] An integer 0..3

@see parity_array

# File lib/sixword/lib.rb, line 197
def self.parity_int(int)
  parity = 0
  while int > 0
    parity += int & 0b11
    int >>= 2
  end

  parity & 0b11
end
word_to_bits(word) click to toggle source

Given a word, return the 11 bits it represents as an integer (i.e. its index in the WORDS list).

@param word [String] @return [Fixnum] An integer 0..2047

# File lib/sixword/lib.rb, line 154
def self.word_to_bits(word)
  word = word.upcase
  return WORDS_HASH.fetch(word)
rescue KeyError
  if (1..4).include?(word.length)
    raise UnknownWord.new("Unknown word: #{word.inspect}")
  else
    raise InvalidWord.new("Word must be 1-4 chars, not #{word.inspect}")
  end
end