Class: EncodedId::Encoders::Hashid

Inherits:
Object
  • Object
show all
Includes:
HashidConsistentShuffle
Defined in:
lib/encoded_id/encoders/hashid.rb

Overview

Implementation of HashId, optimised and adapted from the original ‘hashid.rb` gem

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HashidConsistentShuffle

#consistent_shuffle!

Constructor Details

#initialize(salt, min_hash_length = 0, alphabet = Alphabet.alphanum, blocklist = nil, blocklist_mode = :length_threshold, blocklist_max_length = 32) ⇒ Hashid

Initialize a new HashId encoder with custom parameters.

The initialization process sets up the character sets (alphabet, separators, guards) that will be used for encoding and decoding. These character sets are:

  1. Shuffled based on the salt for uniqueness

  2. Balanced in ratios (alphabet:separators ≈ 3.5:1, alphabet:guards ≈ 12:1)

  3. Made disjoint (no character appears in multiple sets)

Parameters:

  • salt (String)

    Secret salt used to shuffle the alphabet (empty string is valid)

  • min_hash_length (Integer) (defaults to: 0)

    Minimum length of generated hashes (0 for no minimum)

  • alphabet (Alphabet) (defaults to: Alphabet.alphanum)

    Character set to use for encoding

  • blocklist (Blocklist?) (defaults to: nil)

    Optional list of words that shouldn’t appear in hashes

  • blocklist_mode (Symbol) (defaults to: :length_threshold)

    Mode for blocklist checking (:always, :length_threshold, :raise_if_likely)

  • blocklist_max_length (Integer) (defaults to: 32)

    Maximum ID length for blocklist checking (when mode is :length_threshold)



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/encoded_id/encoders/hashid.rb', line 103

def initialize(salt, min_hash_length = 0, alphabet = Alphabet.alphanum, blocklist = nil, blocklist_mode = :length_threshold, blocklist_max_length = 32)
  unless min_hash_length.is_a?(Integer) && min_hash_length >= 0
    raise ArgumentError, "The min length must be a Integer and greater than or equal to 0"
  end
  @min_hash_length = min_hash_length
  @salt = salt
  @alphabet = alphabet
  @blocklist = blocklist
  @blocklist_mode = blocklist_mode
  @blocklist_max_length = blocklist_max_length

  @separators_and_guards = HashidOrdinalAlphabetSeparatorGuards.new(alphabet, salt)
  @alphabet_ordinals = @separators_and_guards.alphabet
  @separator_ordinals = @separators_and_guards.seps
  @guard_ordinals = @separators_and_guards.guards
  @salt_ordinals = @separators_and_guards.salt

  # Pre-compute escaped versions for use with String#tr during decoding.
  # This escapes special regex characters like '-', '\\', and '^' for safe use in tr().
  @escaped_separator_selector = @separators_and_guards.seps_tr_selector
  @escaped_guards_selector = @separators_and_guards.guards_tr_selector
end

Instance Attribute Details

#alphabetObject (readonly)

: Alphabet



131
132
133
# File 'lib/encoded_id/encoders/hashid.rb', line 131

def alphabet
  @alphabet
end

#alphabet_ordinalsObject (readonly)

: Array



126
127
128
# File 'lib/encoded_id/encoders/hashid.rb', line 126

def alphabet_ordinals
  @alphabet_ordinals
end

#blocklistObject (readonly)

: Blocklist?



132
133
134
# File 'lib/encoded_id/encoders/hashid.rb', line 132

def blocklist
  @blocklist
end

#guard_ordinalsObject (readonly)

: Array



128
129
130
# File 'lib/encoded_id/encoders/hashid.rb', line 128

def guard_ordinals
  @guard_ordinals
end

#min_hash_lengthObject (readonly)

: Integer



133
134
135
# File 'lib/encoded_id/encoders/hashid.rb', line 133

def min_hash_length
  @min_hash_length
end

#saltObject (readonly)

: String



130
131
132
# File 'lib/encoded_id/encoders/hashid.rb', line 130

def salt
  @salt
end

#salt_ordinalsObject (readonly)

: Array



129
130
131
# File 'lib/encoded_id/encoders/hashid.rb', line 129

def salt_ordinals
  @salt_ordinals
end

#separator_ordinalsObject (readonly)

: Array



127
128
129
# File 'lib/encoded_id/encoders/hashid.rb', line 127

def separator_ordinals
  @separator_ordinals
end

Instance Method Details

#decode(hash) ⇒ Array<Integer>

Decode a hash string back into an array of integers.

The decoding process:

  1. Removes guards by replacing them with spaces and splitting

  2. Extracts the lottery character (first character after guard removal)

  3. Splits on separators to get individual encoded number segments

  4. For each segment, shuffles the alphabet the same way as encoding and decodes

  5. Verifies by re-encoding the result and comparing to the original hash

This verification step is critical for valid decoding: it ensures that random strings won’t decode to valid numbers. Only properly encoded hashes will pass.

Parameters:

  • hash (String)

    The hash string to decode

Returns:

  • (Array<Integer>)

    Array of decoded integers (empty if hash is invalid)



182
183
184
185
186
# File 'lib/encoded_id/encoders/hashid.rb', line 182

def decode(hash)
  return [] if hash.nil? || hash.empty?

  internal_decode(hash)
end

#encode(numbers) ⇒ String

Encode an array of non-negative integers into a hash string.

The encoding process:

  1. Validates all numbers are integers and non-negative

  2. Calculates a “lottery” character based on the input numbers

  3. For each number, shuffles the alphabet and encodes the number in that custom base

  4. Inserts separator characters between encoded numbers

  5. Adds guards and padding if needed to meet minimum length

  6. Validates the result doesn’t contain blocklisted words

Parameters:

  • numbers (Array<Integer>)

    Array of non-negative integers to encode

Returns:

  • (String)

    The encoded hash string (empty if input is empty or contains negatives)

Raises:

  • (BlocklistError)

    If the generated hash contains a blocklisted word



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/encoded_id/encoders/hashid.rb', line 150

def encode(numbers)
  numbers.all? { |n| Integer(n) }

  return "" if numbers.empty? || numbers.any? { |n| n < 0 }

  encoded = internal_encode(numbers)
  if check_blocklist?(encoded)
    blocked_word = contains_blocklisted_word?(encoded)
    if blocked_word
      raise EncodedId::BlocklistError, "Generated ID '#{encoded}' contains blocklisted word: '#{blocked_word}'"
    end
  end

  encoded
end