Sha256: b25208da3f05b258b0c9d4268a1bbf0cdadea8d2808da38a7c9b2b1cccefba2b

Contents?: true

Size: 1.65 KB

Versions: 2

Compression:

Stored size: 1.65 KB

Contents

# frozen_string_literal: true
module Tokogen
  class Generator
    DEFAULT_ALPHABET = (('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a).join.freeze

    attr_reader :randomness_source

    def initialize(randomness_source:, alphabet: DEFAULT_ALPHABET)
      @randomness_source = randomness_source
      @alphabet = alphabet
    end

    def generate(length)
      token_bits_amount = length * bits_per_char
      bytes_to_read = full_bytes_in_bits(token_bits_amount)
      bytes = random_bytes(bytes_to_read)
      bits = bytes.unpack('b*')[0]
      # It's possible we've read a couple exta bits of randomness,
      # since randomness is rounded to bytes.
      # Here we only take first `length` of bit that we need.
      bit_string_split(bits, bits_per_char)
        .take(length)
        .map { |index| alphabet_char(index) }
        .join
    end

    def random_bytes(size)
      @randomness_source.random_bytes(size)
    end

    def max_char_index
      @alphabet.size - 1
    end

    def bits_per_char
      max_char_index.bit_length
    end

    def full_bytes_in_bits(bits)
      (bits + 7) >> 3
    end

    private

    def bit_string_split(bits, bits_per_char, &block) # rubocop:disable Metrics/MethodLength
      top = max_char_index
      curry = 0
      last_curry = 0
      bits.each_char.each_slice(bits_per_char).map do |binary_ord|
        val = binary_ord.join.to_i(2) + curry
        last_curry = curry
        if val <= top
          current = val
          curry = 0
        else
          current = top
          curry = val % top
        end
        current
      end.each(&block)
    end

    def alphabet_char(index)
      @alphabet[index]
    end
  end
end

Version data entries

2 entries across 2 versions & 1 rubygems

Version Path
tokogen-0.1.2 lib/tokogen/generator.rb
tokogen-0.1.1 lib/tokogen/generator.rb