hashids 0.0.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +51 -22
  4. data/lib/hashids.rb +188 -160
  5. data/spec/hashids_spec.rb +185 -55
  6. metadata +3 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4aca5b2a67715e91ddbfc9b5c68cfd794a6c986a
4
- data.tar.gz: 23974c9688a19b0da1215bbdc9065071234cd903
3
+ metadata.gz: 55450f1c6ad19dcf1420aaa46d62003216197f21
4
+ data.tar.gz: ea41a9497fe51dc028af387210e716627a03890c
5
5
  SHA512:
6
- metadata.gz: 8e7f1e742554373ce8fbb3128c59ccb5e4992a95624b5115649ccd7cd9e1cd90078bdd04d611f2e0f71e8d6d0be2cd659bba453140483fa54b263250e7137243
7
- data.tar.gz: 62132383ecd62f42c0d9ecd8a2684a746bf1fd6e8dd99f2c5e5ad06c72941359a1aec729f00f443dab42b7a5b62d5a29c23ab7e3151c3299ecdcd956fb86dbf3
6
+ metadata.gz: 34d65939ac98b5a38e9f518cf8dd0da07a3c760e49cad4df4d05cec4952f0177c58b982b871c7dc5d75eef9cb1ba766c7ed197c169eb1b9ddb570f1682831e49
7
+ data.tar.gz: 7c5c66153a745d579810d14815db3a60a2fc3d02c15bf868dc60077d898ee3ab7c614189a7772b3040847b3a226feca975a9067d51246673da6717d31b1688d9
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Peter Hellberg
1
+ Copyright (c) 2013 Peter Hellberg
2
2
 
3
3
  MIT License
4
4
 
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -24,7 +24,7 @@ This algorithm tries to satisfy the following requirements:
24
24
  3. You should be able to specify minimum hash length.
25
25
  4. Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL).
26
26
 
27
- Instead of showing items as `1`, `2`, or `3`, you could show them as `U6dc`, `u87U`, and `HMou`.
27
+ Instead of showing items as `1`, `2`, or `3`, you could show them as `jR`, `k5`, and `l5`.
28
28
  You don't have to store these hashes in the database, but can encrypt + decrypt on the fly.
29
29
 
30
30
  All integers need to be greater than or equal to zero.
@@ -47,7 +47,8 @@ Or install it yourself as:
47
47
 
48
48
  ### Encrypting one number
49
49
 
50
- You can pass a unique salt value so your hashes differ from everyone else's. I use "**this is my salt**" as an example.
50
+ You can pass a unique salt value so your hashes differ from everyone else's.
51
+ I use **this is my salt** as an example.
51
52
 
52
53
  ```ruby
53
54
  hashids = Hashids.new("this is my salt")
@@ -56,7 +57,7 @@ hash = hashids.encrypt(12345)
56
57
 
57
58
  `hash` is now going to be:
58
59
 
59
- ryBo
60
+ NkK9
60
61
 
61
62
  ### Decrypting
62
63
 
@@ -64,7 +65,7 @@ Notice during decryption, same salt value is used:
64
65
 
65
66
  ```ruby
66
67
  hashids = Hashids.new("this is my salt")
67
- numbers = hashids.decrypt("ryBo")
68
+ numbers = hashids.decrypt("NkK9")
68
69
  ```
69
70
 
70
71
  `numbers` is now going to be:
@@ -77,7 +78,7 @@ Decryption will not work if salt is changed:
77
78
 
78
79
  ```ruby
79
80
  hashids = Hashids.new("this is my pepper")
80
- numbers = hashids.decrypt("ryBo")
81
+ numbers = hashids.decrypt("NkK9")
81
82
  ```
82
83
 
83
84
  `numbers` is now going to be:
@@ -93,13 +94,13 @@ hash = hashids.encrypt(683, 94108, 123, 5)
93
94
 
94
95
  `hash` is now going to be:
95
96
 
96
- zBphL54nuMyu5
97
+ aBMswoO2UB3Sj
97
98
 
98
99
  ### Decrypting is done the same way
99
100
 
100
101
  ```ruby
101
102
  hashids = Hashids.new("this is my salt")
102
- numbers = hashids.decrypt("zBphL54nuMyu5")
103
+ numbers = hashids.decrypt("aBMswoO2UB3Sj")
103
104
  ```
104
105
 
105
106
  `numbers` is now going to be:
@@ -108,7 +109,8 @@ numbers = hashids.decrypt("zBphL54nuMyu5")
108
109
 
109
110
  ### Encrypting and specifying minimum hash length
110
111
 
111
- Here we encrypt integer 1, and set the minimum hash length to **8** (by default it's **0** -- meaning hashes will be the shortest possible length).
112
+ Here we encrypt integer 1, and set the minimum hash length to **8**
113
+ (by default it's **0** -- meaning hashes will be the shortest possible length).
112
114
 
113
115
  ```ruby
114
116
  hashids = Hashids.new("this is my salt", 8)
@@ -117,13 +119,13 @@ hash = hashids.encrypt(1)
117
119
 
118
120
  `hash` is now going to be:
119
121
 
120
- b9iLXiAa
122
+ gB0NV05e
121
123
 
122
- ### Decrypting
124
+ ### Decrypting with minimum hash length
123
125
 
124
126
  ```ruby
125
127
  hashids = Hashids.new("this is my salt", 8)
126
- numbers = hashids.decrypt("b9iLXiAa")
128
+ numbers = hashids.decrypt("gB0NV05e")
127
129
  ```
128
130
 
129
131
  `numbers` is now going to be:
@@ -132,16 +134,16 @@ numbers = hashids.decrypt("b9iLXiAa")
132
134
 
133
135
  ### Specifying custom hash alphabet
134
136
 
135
- Here we set the alphabet to consist of only four letters: "abcd"
137
+ Here we set the alphabet to consist of: "abcdefghijkABCDEFGHIJK12345"
136
138
 
137
139
  ```ruby
138
- hashids = Hashids.new("this is my salt", 0, "abcd")
140
+ hashids = Hashids.new("this is my salt", 0, "abcdefghijkABCDEFGHIJK12345")
139
141
  hash = hashids.encrypt(1, 2, 3, 4, 5)
140
142
  ```
141
143
 
142
144
  `hash` is now going to be:
143
145
 
144
- adcdacddcdaacdad
146
+ dEc4iEHeF3
145
147
 
146
148
  ## Randomness
147
149
 
@@ -157,7 +159,7 @@ hash = hashids.encrypt(5, 5, 5, 5)
157
159
 
158
160
  You don't see any repeating patterns that might show there's 4 identical numbers in the hash:
159
161
 
160
- GLh5SMs9
162
+ 1Wc8cwcE
161
163
 
162
164
  Same with incremented numbers:
163
165
 
@@ -166,24 +168,51 @@ hashids = Hashids.new("this is my salt")
166
168
  hash = hashids.encrypt(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
167
169
  ```
168
170
 
169
- `hash` will be :
171
+ `hash` is now going to be:
170
172
 
171
- zEUzfySGIpuyhpF6HaC7
173
+ kRHnurhptKcjIDTWC3sx
172
174
 
173
175
  ### Incrementing number hashes:
174
176
 
175
177
  ```ruby
176
178
  hashids = Hashids.new("this is my salt")
177
179
 
178
- hashids.encrypt 1 #=> LX
179
- hashids.encrypt 2 #=> ed
180
- hashids.encrypt 3 #=> o9
181
- hashids.encrypt 4 #=> 4n
182
- hashids.encrypt 5 #=> a5
180
+ hashids.encrypt 1 #=> NV
181
+ hashids.encrypt 2 #=> 6m
182
+ hashids.encrypt 3 #=> yD
183
+ hashids.encrypt 4 #=> 2l
184
+ hashids.encrypt 5 #=> rD
185
+ ```
186
+
187
+ ### Encrypting using a HEX string
188
+
189
+ ```ruby
190
+ hashids = Hashids.new("this is my salt")
191
+ hash = hashids.encrypt_hex('DEADBEEF')
183
192
  ```
184
193
 
194
+ `hash` is now going to be:
195
+
196
+ kRNrpKlJ
197
+
198
+ ### Decrypting to a HEX string
199
+
200
+ ```ruby
201
+ hashids = Hashids.new("this is my salt")
202
+ hex_str = hashids.decrypt_hex("kRNrpKlJ")
203
+ ```
204
+
205
+ `hex_str` is now going to be:
206
+
207
+ DEADBEEF
208
+
185
209
  ## Changelog
186
210
 
211
+ **0.3.0**
212
+
213
+ - Bumped the version number since hashids.rb now support the new algorithm
214
+ - Support for `encrypt_hex` and `decrypt_hex`
215
+
187
216
  **0.0.3**
188
217
 
189
218
  - Default salt (Allows for `Hashids.new.encrypt(91) #=> "kBy"`)
data/lib/hashids.rb CHANGED
@@ -1,255 +1,283 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  class Hashids
4
- VERSION = "0.0.5"
5
- DEFAULT_ALPHABET = "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz"
6
- PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]
4
+ VERSION = "0.3.0"
7
5
 
8
- SaltError = Class.new(ArgumentError)
9
- MinLengthError = Class.new(ArgumentError)
10
- AlphabetError = Class.new(ArgumentError)
6
+ MIN_ALPHABET_LENGTH = 16
7
+ SEP_DIV = 3.5
8
+ GUARD_DIV = 12.0
11
9
 
12
- def initialize(salt = "", min_length = 0, alphabet = DEFAULT_ALPHABET)
13
- @salt = salt
14
- @min_length = min_length
15
- @alphabet = alphabet
10
+ DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyz" +
11
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
12
+ "1234567890"
16
13
 
17
- @chars_regex = /./
14
+ attr_reader :salt, :min_hash_length, :alphabet, :seps, :guards
15
+
16
+ def initialize(salt = "", min_hash_length = 0, alphabet = DEFAULT_ALPHABET)
17
+ @salt = salt
18
+ @min_hash_length = min_hash_length
19
+ @alphabet = alphabet
18
20
 
19
- validate_attributes
20
21
  setup_alphabet
21
22
  end
22
23
 
23
24
  def encrypt(*numbers)
24
25
  numbers.flatten! if numbers.length == 1
25
26
 
26
- if numbers.empty? || numbers.reject { |n| Integer(n) && n > 0 }.any?
27
+ if numbers.empty? || numbers.reject { |n| Integer(n) && n >= 0 }.any?
27
28
  ""
28
29
  else
29
- encode(numbers, @alphabet, @salt, @min_length)
30
+ encode(numbers)
30
31
  end
31
32
  end
32
33
 
33
- def decrypt(hash)
34
- hash.empty? ? [] : decode(hash)
34
+ def encrypt_hex(str)
35
+ return "" unless str.to_s.match(/\A[0-9a-fA-F]+\Z/)
36
+
37
+ numbers = str.scan(/[\w\W]{1,12}/).map do |num|
38
+ "1#{num}".to_i(16)
39
+ end
40
+
41
+ encrypt(numbers)
35
42
  end
36
43
 
37
- private
44
+ def decrypt(hash)
45
+ return [] if hash.nil? || hash.empty?
38
46
 
39
- def validate_attributes
40
- unless @salt.kind_of?(String)
41
- raise Hashids::SaltError, "The salt must be a String"
42
- end
47
+ decode(hash, @alphabet)
48
+ end
43
49
 
44
- unless @min_length.kind_of?(Fixnum)
45
- raise Hashids::MinLengthError, "The min length must be a Fixnum"
46
- end
50
+ def decrypt_hex(hash)
47
51
 
48
- unless @min_length >= 0
49
- raise Hashids::MinLengthError, "The min length must be 0 or more"
50
- end
52
+ ret = ""
53
+ numbers = decrypt(hash)
51
54
 
52
- unless @alphabet.kind_of?(String)
53
- raise Hashids::AlphabetError, "The alphabet must be a String"
55
+ numbers.length.times do |i|
56
+ ret += numbers[i].to_s(16)[1 .. -1]
54
57
  end
58
+
59
+ ret.upcase
55
60
  end
56
61
 
57
- def setup_alphabet
58
- @seps = []
59
- @guards = []
62
+ protected
63
+
64
+ def encode(numbers)
65
+ ret = ""
60
66
 
61
- @alphabet = @alphabet.scan(@chars_regex).uniq.join('')
67
+ alphabet = @alphabet
68
+ length = numbers.length
69
+ hash_int = 0
62
70
 
63
- if @alphabet.length < 4
64
- raise AlphabetError, "Alphabet must contain at least 4 unique characters."
71
+ length.times do |i|
72
+ hash_int += (numbers[i] % (i + 100))
65
73
  end
66
74
 
67
- PRIMES.each do |prime|
68
- char = @alphabet[prime - 1]
75
+ lottery = ret = alphabet[hash_int % alphabet.length]
76
+
77
+ length.times do |i|
78
+ num = numbers[i]
79
+ buf = lottery + salt + alphabet
80
+
81
+ alphabet = consistent_shuffle(alphabet, buf[0, alphabet.length])
82
+ last = hash(num, alphabet)
69
83
 
70
- break if char.nil?
84
+ ret += last
71
85
 
72
- @seps << char
73
- @alphabet.tr!(char, ' ')
86
+ if (i + 1) < length
87
+ num %= (last.ord + i)
88
+ ret += seps[num % seps.length]
89
+ end
74
90
  end
75
91
 
76
- [0, 4, 8, 12].each do |index|
77
- separator = @seps[index]
92
+ if ret.length < min_hash_length
93
+ ret = guards[(hash_int + ret[0].ord) % guards.length] + ret
78
94
 
79
- unless separator.nil?
80
- @guards << separator
81
- @seps.delete_at(index)
95
+ if ret.length < min_hash_length
96
+ ret += guards[(hash_int + ret[2].ord) % guards.length]
82
97
  end
83
98
  end
84
99
 
85
- @alphabet.delete!(' ')
86
- @alphabet = consistent_shuffle(@alphabet, @salt)
87
- end
100
+ half_length = alphabet.length.div(2)
88
101
 
89
- def encode(numbers, alphabet, salt, min_length = 0)
90
- ret = ""
102
+ while(ret.length < min_hash_length)
103
+ alphabet = consistent_shuffle(alphabet, alphabet)
104
+ ret = alphabet[half_length .. -1] + ret + alphabet[0, half_length]
91
105
 
92
- seps = consistent_shuffle(@seps, numbers).scan(@chars_regex)
93
- lottery_char = ""
106
+ excess = ret.length - min_hash_length
107
+ ret = ret[excess / 2, min_hash_length] if excess > 0
108
+ end
94
109
 
95
- numbers.each_with_index do |number, i|
96
- if i == 0
97
- lottery_salt = numbers.join('-')
98
- numbers.each { |n| lottery_salt += "-#{(n + 1) * 2}" }
110
+ ret
111
+ end
99
112
 
100
- lottery = consistent_shuffle(alphabet, lottery_salt)
113
+ def decode(hash, alphabet)
114
+ ret = []
101
115
 
102
- ret += lottery_char = lottery[0]
116
+ breakdown = hash.gsub(/[#{@guards}]/, " ")
117
+ array = breakdown.split(" ")
103
118
 
104
- alphabet = "#{lottery_char}#{alphabet.delete(lottery_char)}"
105
- end
119
+ i = [3,2].include?(array.length) ? 1 : 0
106
120
 
107
- alphabet = consistent_shuffle(alphabet, "#{lottery_char.ord & 12345}#{salt}")
108
- ret += hash(number, alphabet)
121
+ if breakdown = array[i]
122
+ lottery = breakdown[0]
123
+ breakdown = breakdown[1 .. -1].gsub(/[#{@seps}]/, " ")
124
+ array = breakdown.split(" ")
109
125
 
110
- if (i + 1) < numbers.length
111
- seps_index = (number + i) % seps.length
112
- ret += seps[seps_index]
126
+ array.length.times do |i|
127
+ sub_hash = array[i]
128
+ buffer = lottery + salt + alphabet
129
+ alphabet = consistent_shuffle(alphabet, buffer[0, alphabet.length])
130
+
131
+ ret.push unhash(sub_hash, alphabet)
113
132
  end
114
- end
115
133
 
116
- if ret.length < min_length
117
- first_index = 0
118
- numbers.each_with_index do |number, i|
119
- first_index += (i + 1) * number
134
+ if encode(ret) != hash
135
+ ret = []
120
136
  end
137
+ end
121
138
 
122
- guard_index = first_index % @guards.length
123
- guard = @guards[guard_index]
139
+ ret
140
+ end
124
141
 
125
- ret = guard + ret
142
+ def consistent_shuffle(alphabet, salt)
143
+ return alphabet if salt.nil? || salt.empty?
126
144
 
127
- if ret.length < min_length
128
- guard_index = (guard_index + ret.length) % @guards.length
129
- guard = @guards[guard_index]
145
+ v = 0
146
+ p = 0
130
147
 
131
- ret += guard
132
- end
133
- end
148
+ (alphabet.length-1).downto(1) do |i|
149
+ v = v % salt.length
150
+ p += n = salt[v].ord
151
+ j = (n + v + p) % i
134
152
 
135
- while ret.length < min_length
136
- pad_array = [alphabet[1].ord, alphabet[0].ord]
137
- pad_left = encode(pad_array, alphabet, salt)
138
- pad_right = encode(pad_array, alphabet, pad_array.join(''))
153
+ tmp_char = alphabet[j]
139
154
 
140
- ret = "#{pad_left}#{ret}#{pad_right}"
141
- excess = ret.length - min_length
155
+ alphabet = alphabet[0, j] + alphabet[i] + alphabet[j + 1..-1]
156
+ alphabet = alphabet[0, i] + tmp_char + alphabet[i + 1..-1]
142
157
 
143
- ret = ret[(excess.div(2)), min_length] if excess > 0
144
- alphabet = consistent_shuffle(alphabet, salt + ret)
158
+ v += 1
145
159
  end
146
160
 
147
- ret
161
+ alphabet
148
162
  end
149
163
 
150
- def decode(hash)
151
- ret = []
164
+ def hash(input, alphabet)
165
+ num = input.to_i
166
+ len = alphabet.length
167
+ res = ""
152
168
 
153
- if hash.length > 0
154
- original_hash = hash
155
- alphabet = ""
156
- lottery_char = ""
169
+ begin
170
+ res = "#{alphabet[num % len]}#{res}"
171
+ num = num.div(alphabet.length)
172
+ end while num > 0
157
173
 
158
- hash_split = hash.tr(@guards.join(''), ' ').split(' ')
159
- hash = hash_split[[3,2].include?(hash_split.length) ? 1 : 0]
174
+ res
175
+ end
160
176
 
161
- hash.tr!(@seps.join(''), ' ')
177
+ def unhash(input, alphabet)
178
+ num = 0
162
179
 
163
- hash_array = hash.split(' ')
180
+ input.length.times do |i|
181
+ pos = alphabet.index(input[i])
182
+ num += pos * alphabet.length ** (input.length - i - 1)
183
+ end
164
184
 
165
- hash_array.each_with_index do |sub_hash, i|
166
- if sub_hash.length > 0
167
- if i == 0
168
- lottery_char = hash[0]
169
- sub_hash = sub_hash[1..-1]
170
- alphabet = lottery_char + @alphabet.delete(lottery_char)
171
- end
185
+ num
186
+ end
172
187
 
173
- if alphabet.length > 0 && lottery_char.length > 0
174
- alphabet = consistent_shuffle(alphabet, (lottery_char.ord & 12345).to_s + @salt)
175
- ret << unhash(sub_hash, alphabet)
176
- end
177
- end
178
- end
188
+ private
179
189
 
180
- ret = [] if encrypt(*ret) != original_hash
181
- end
190
+ def setup_alphabet
191
+ validate_attributes
182
192
 
183
- ret
193
+ @alphabet = alphabet.split('').uniq.join('')
194
+
195
+ setup_seps
196
+ setup_guards
197
+
198
+ validate_alphabet
184
199
  end
185
200
 
186
- def consistent_shuffle(alphabet, salt)
187
- ret = ""
201
+ def setup_seps
202
+ @seps = "cfhistuCFHISTU"
188
203
 
189
- alphabet = alphabet.join("") if alphabet.respond_to? :join
190
- salt = salt.join("") if salt.respond_to? :join
204
+ # seps should contain only characters present in alphabet
205
+ # alphabet should not contains seps
191
206
 
192
- alphabet_array = alphabet.scan(@chars_regex)
193
- salt_array = salt.scan(@chars_regex)
194
- sorting_array = []
207
+ seps.length.times do |i|
208
+ j = @alphabet.index(seps[i])
195
209
 
196
- salt_array << 0 if salt_array.empty?
197
- salt_array.each { |char| sorting_array << char.ord }
210
+ if j.nil?
211
+ @seps = seps[0, i] + " " + seps[i + 1 .. -1]
212
+ else
213
+ @alphabet = alphabet[0, j] + " " + alphabet[j + 1 .. -1]
214
+ end
215
+ end
198
216
 
199
- sorting_array.each_with_index do |int,i|
200
- add = true
201
- k = i
217
+ alphabet.delete!(' ')
218
+ seps.delete!(' ')
202
219
 
203
- while k != sorting_array.length + i - 1
204
- next_index = (k + 1) % sorting_array.length
220
+ @seps = consistent_shuffle(seps, salt)
205
221
 
206
- if add
207
- sorting_array[i] += sorting_array[next_index] + (k * i)
208
- else
209
- sorting_array[i] -= sorting_array[next_index]
210
- end
222
+ if seps.length == 0 || (alphabet.length / seps.length.to_f) > SEP_DIV
223
+ seps_length = (alphabet.length / SEP_DIV).ceil
224
+ seps_length = 2 if seps_length == 1
211
225
 
212
- add = !add
213
- k += 1
214
- end
226
+ if seps_length > seps.length
227
+ diff = seps_length - seps.length;
215
228
 
216
- sorting_array[i] = sorting_array[i].abs
229
+ @seps += alphabet[0, diff]
230
+ @alphabet = alphabet[diff .. -1]
231
+ else
232
+ @seps = seps[0, seps_length]
233
+ end
217
234
  end
218
235
 
219
- i = 0
236
+ @alphabet = consistent_shuffle(alphabet, salt)
237
+ end
220
238
 
221
- while alphabet_array.length > 0
222
- pos = sorting_array[i]
223
- pos %= alphabet_array.length if pos >= alphabet_array.length
224
- ret += alphabet_array[pos]
239
+ def setup_guards
240
+ gc = (alphabet.length / GUARD_DIV).ceil
225
241
 
226
- alphabet_array.delete_at(pos)
227
- i = (i+1) % sorting_array.length
242
+ if alphabet.length < 3
243
+ @guards = seps[0, gc]
244
+ @seps = seps[gc .. -1]
245
+ else
246
+ @guards = alphabet[0, gc]
247
+ @alphabet = alphabet[gc .. -1]
228
248
  end
229
-
230
- ret
231
249
  end
232
250
 
233
- def hash(number, alphabet)
234
- hash = ""
251
+ SaltError = Class.new(ArgumentError)
252
+ MinLengthError = Class.new(ArgumentError)
253
+ AlphabetError = Class.new(ArgumentError)
235
254
 
236
- while number > 0
237
- hash = "#{alphabet[number % alphabet.length]}#{hash}"
238
- number = number.div(alphabet.length)
255
+ def validate_attributes
256
+ unless salt.kind_of?(String)
257
+ raise SaltError, "The salt must be a String"
239
258
  end
240
259
 
241
- hash
242
- end
260
+ unless min_hash_length.kind_of?(Fixnum)
261
+ raise MinLengthError, "The min length must be a Fixnum"
262
+ end
243
263
 
244
- def unhash(hash, alphabet)
245
- number = 0
264
+ unless min_hash_length >= 0
265
+ raise MinLengthError, "The min length must be 0 or more"
266
+ end
246
267
 
247
- hash.scan(@chars_regex).each_with_index do |char, i|
248
- if pos = alphabet.index(char)
249
- number += pos * alphabet.length ** (hash.length - i - 1)
250
- end
268
+ unless alphabet.kind_of?(String)
269
+ raise AlphabetError, "The alphabet must be a String"
251
270
  end
252
271
 
253
- number
272
+ if alphabet.include?(' ')
273
+ raise AlphabetError, "The alphabet can’t include spaces"
274
+ end
275
+ end
276
+
277
+ def validate_alphabet
278
+ unless alphabet.length >= MIN_ALPHABET_LENGTH
279
+ raise AlphabetError, "Alphabet must contain at least " +
280
+ "#{MIN_ALPHABET_LENGTH} unique characters."
281
+ end
254
282
  end
255
283
  end
data/spec/hashids_spec.rb CHANGED
@@ -8,74 +8,98 @@ require_relative "../lib/hashids"
8
8
 
9
9
  describe Hashids do
10
10
  let(:salt) { 'this is my salt' }
11
+ let(:seps) { 'UHuhtcITCsFifS' }
12
+ let(:guards) { 'AdG0' }
11
13
  let(:hashids) { Hashids.new(salt) }
12
- let(:alphabet) { "xgzXjp48RaoeLdB6yrAMK9b5n7qEkG" }
13
- let(:shuffled_alphabet) { "xMGAE6gXkpBR785zLraqnj49doyKeb" }
14
- let(:default_alphabet) { "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz" }
15
- let(:seps) { ["S", "F", "h", "U", "I", "t", "u", "C", "H", "f", "s"] }
16
- let(:examples) {
17
- {
18
- 'aa' => 155, 'ba' => 185, 'j9' => 814,
19
- 'ky' => 342, 'Xg' => 559, 'GB' => 683,
20
- 'B4' => 691, '9Xg' => 4159, 'jAz' => 24599
21
- }
14
+
15
+ let(:default_alphabet) {
16
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
22
17
  }
23
18
 
24
- describe "setup" do
25
- it "has a default salt" do
26
- Hashids.new.encrypt(1,2,3).must_equal "katKSA"
27
- end
19
+ let(:alphabet) {
20
+ "5N6y2rljDQak4xgzn8ZR1oKYLmJpEbVq3OBv9WwXPMe7"
21
+ }
28
22
 
23
+ describe "setup" do
29
24
  it "has a default alphabet" do
30
25
  Hashids::DEFAULT_ALPHABET.must_equal default_alphabet
31
26
  end
32
27
 
28
+ it "has a default salt" do
29
+ Hashids.new.encrypt(1,2,3).must_equal "o2fXhV"
30
+ end
31
+
33
32
  it "has the correct salt" do
34
33
  hashids.instance_variable_get(:@salt).must_equal salt
35
34
  end
36
35
 
37
36
  it "defaults to a min_length of 0" do
38
- hashids.instance_variable_get(:@min_length).must_equal 0
37
+ hashids.instance_variable_get(:@min_hash_length).must_equal 0
39
38
  end
40
39
 
41
- it "generates the correct @seps" do
40
+ it "generates the correct seps" do
42
41
  hashids.instance_variable_get(:@seps).must_equal seps
43
42
  end
44
43
 
45
44
  it "generates the correct @guards" do
46
- hashids.instance_variable_get(:@guards).must_equal ["c", "i", "T"]
45
+ hashids.instance_variable_get(:@guards).must_equal guards
47
46
  end
48
47
 
49
48
  it "generates the correct alphabet" do
50
49
  hashids.instance_variable_get(:@alphabet).must_equal alphabet
51
50
  end
51
+
52
+ it "has a minimum alphabet length" do
53
+ -> {
54
+ Hashids.new("", 0, 'shortalphabet')
55
+ }.must_raise Hashids::AlphabetError
56
+ end
57
+
58
+ it "checks the alphabet for spaces" do
59
+ -> {
60
+ Hashids.new("", 0, 'abc odefghijklmnopqrstuv')
61
+ }.must_raise Hashids::AlphabetError
62
+ end
52
63
  end
53
64
 
54
65
  describe "encrypt" do
55
66
  it "encrypts a single number" do
56
- hashids.encrypt(12345).must_equal 'ryBo'
57
-
58
- hashids.tap { |h|
59
- h.encrypt(-1).must_equal ''
60
- h.encrypt(1).must_equal 'LX'
61
- h.encrypt(22).must_equal '5B'
62
- h.encrypt(333).must_equal 'o49'
63
- h.encrypt(9999).must_equal 'GKnB'
64
- }
67
+ hashids.encrypt(12345).must_equal 'NkK9'
68
+
69
+ hashids.tap do |h|
70
+ h.encrypt(-1).must_equal ''
71
+ h.encrypt(1).must_equal 'NV'
72
+ h.encrypt(22).must_equal 'K4'
73
+ h.encrypt(333).must_equal 'OqM'
74
+ h.encrypt(9999).must_equal 'kQVg'
75
+ h.encrypt(123_000).must_equal '58LzD'
76
+ h.encrypt(456_000_000).must_equal '5gn6mQP'
77
+ h.encrypt(987_654_321).must_equal 'oyjYvry'
78
+ end
65
79
  end
66
80
 
67
81
  it "can encrypt a list of numbers" do
68
- hashids.encrypt(683, 94108, 123, 5).must_equal 'zBphL54nuMyu5'
82
+ hashids.tap do |h|
83
+ h.encrypt(1,2,3).must_equal "laHquq"
84
+ h.encrypt(2,4,6).must_equal "44uotN"
85
+ h.encrypt(99,25).must_equal "97Jun"
69
86
 
70
- hashids.tap { |h|
71
- h.encrypt(1,2,3).must_equal 'eGtrS8'
72
- h.encrypt(2,4,6).must_equal '9Kh7fz'
73
- h.encrypt(99,25).must_equal 'dAECX'
74
- }
87
+ h.encrypt(1337,42,314).
88
+ must_equal "7xKhrUxm"
89
+
90
+ h.encrypt(683, 94108, 123, 5).
91
+ must_equal "aBMswoO2UB3Sj"
92
+
93
+ h.encrypt(547, 31, 241271, 311, 31397, 1129, 71129).
94
+ must_equal "3RoSDhelEyhxRsyWpCx5t1ZK"
95
+
96
+ h.encrypt(21979508, 35563591, 57543099, 93106690, 150649789).
97
+ must_equal "p2xkL3CK33JjcrrZ8vsw4YRZueZX9k"
98
+ end
75
99
  end
76
100
 
77
101
  it "can encrypt a list of numbers passed in as an array" do
78
- hashids.encrypt([1,2,3]).must_equal 'eGtrS8'
102
+ hashids.encrypt([1,2,3]).must_equal "laHquq"
79
103
  end
80
104
 
81
105
  it "returns an empty string if no numbers" do
@@ -88,66 +112,107 @@ describe Hashids do
88
112
  end
89
113
 
90
114
  it "can encrypt to a minumum length" do
91
- h = Hashids.new(salt, 8)
92
- h.encrypt(1).must_equal "b9iLXiAa"
115
+ h = Hashids.new(salt, 18)
116
+ h.encrypt(1).must_equal "aJEDngB0NV05ev1WwP"
117
+
118
+ h.encrypt(4140, 21147, 115975, 678570, 4213597, 27644437).
119
+ must_equal "pLMlCWnJSXr1BSpKgqUwbJ7oimr7l6"
93
120
  end
94
121
 
95
122
  it "can encrypt with a custom alphabet" do
96
- h = Hashids.new(salt, 0, "abcd")
97
- h.encrypt(1,2,3,4,5).must_equal 'adcdacddcdaacdad'
123
+ h = Hashids.new(salt, 0, "ABCDEFGhijklmn34567890-:")
124
+ h.encrypt(1,2,3,4,5).must_equal "6nhmFDikA0"
98
125
  end
99
126
 
100
127
  it "does not produce repeating patterns for identical numbers" do
101
- hashids.encrypt(5,5,5,5).must_equal 'GLh5SMs9'
128
+ hashids.encrypt(5,5,5,5).must_equal "1Wc8cwcE"
102
129
  end
103
130
 
104
131
  it "does not produce repeating patterns for incremented numbers" do
105
- hashids.encrypt(*(1..10).to_a).must_equal 'zEUzfySGIpuyhpF6HaC7'
132
+ hashids.encrypt(*(1..10).to_a).must_equal "kRHnurhptKcjIDTWC3sx"
106
133
  end
107
134
 
108
135
  it "does not produce similarities between incrementing number hashes" do
109
- hashids.encrypt(1).must_equal 'LX'
110
- hashids.encrypt(2).must_equal 'ed'
111
- hashids.encrypt(3).must_equal 'o9'
112
- hashids.encrypt(4).must_equal '4n'
113
- hashids.encrypt(5).must_equal 'a5'
136
+ hashids.encrypt(1).must_equal 'NV'
137
+ hashids.encrypt(2).must_equal '6m'
138
+ hashids.encrypt(3).must_equal 'yD'
139
+ hashids.encrypt(4).must_equal '2l'
140
+ hashids.encrypt(5).must_equal 'rD'
141
+ end
142
+ end
143
+
144
+ describe "encrypt_hex" do
145
+ it "encrypts hex string" do
146
+ hashids.tap { |h|
147
+ h.encrypt_hex("FA").must_equal "lzY"
148
+ h.encrypt_hex("26dd").must_equal "MemE"
149
+ h.encrypt_hex("FF1A").must_equal "eBMrb"
150
+ h.encrypt_hex("12abC").must_equal "D9NPE"
151
+ h.encrypt_hex("185b0").must_equal "9OyNW"
152
+ h.encrypt_hex("17b8d").must_equal "MRWNE"
153
+
154
+ h.encrypt_hex("1d7f21dd38").must_equal "4o6Z7KqxE"
155
+ h.encrypt_hex("20015111d").must_equal "ooweQVNB"
156
+ }
157
+ end
158
+
159
+ it "returns an empty string if passed non-hex string" do
160
+ hashids.encrypt_hex("XYZ123").must_equal ""
114
161
  end
115
162
  end
116
163
 
117
164
  describe "decrypt" do
118
165
  it "decrypts an encrypted number" do
119
- hashids.decrypt("ryBo").must_equal [12345]
166
+ hashids.decrypt("NkK9").must_equal [12345]
167
+ hashids.decrypt("5O8yp5P").must_equal [666555444]
168
+ hashids.decrypt("KVO9yy1oO5j").must_equal [666555444333222]
120
169
 
121
170
  hashids.tap { |h|
122
- h.decrypt('qkpA').must_equal [1337]
123
- h.decrypt('6aX').must_equal [808]
124
- h.decrypt('gz9').must_equal [303]
171
+ h.decrypt("Wzo").must_equal [1337]
172
+ h.decrypt("DbE").must_equal [808]
173
+ h.decrypt("yj8").must_equal [303]
125
174
  }
126
175
  end
127
176
 
128
177
  it "decrypts a list of encrypted numbers" do
129
- hashids.decrypt('zBphL54nuMyu5').must_equal [683, 94108, 123, 5]
178
+ hashids.decrypt("1gRYUwKxBgiVuX").must_equal [66655,5444333,2,22]
179
+ hashids.decrypt('aBMswoO2UB3Sj').must_equal [683, 94108, 123, 5]
130
180
 
131
181
  hashids.tap { |h|
132
- h.decrypt('kEFy').must_equal [1, 2]
133
- h.decrypt('Aztn').must_equal [6, 5]
182
+ h.decrypt('jYhp').must_equal [3, 4]
183
+ h.decrypt('k9Ib').must_equal [6, 5]
184
+
185
+ h.decrypt('EMhN').must_equal [31, 41]
186
+ h.decrypt('glSgV').must_equal [13, 89]
134
187
  }
135
188
  end
136
189
 
137
190
  it "does not decrypt with a different salt" do
138
191
  peppers = Hashids.new('this is my pepper')
139
- hashids.decrypt('ryBo').must_equal [12345]
140
- peppers.decrypt('ryBo').must_equal []
192
+
193
+ hashids.decrypt('NkK9').must_equal [12345]
194
+ peppers.decrypt('NkK9').must_equal []
141
195
  end
142
196
 
143
197
  it "can decrypt from a hash with a minimum length" do
144
198
  h = Hashids.new(salt, 8)
145
- h.decrypt("b9iLXiAa").must_equal [1]
199
+ h.decrypt("gB0NV05e").must_equal [1]
200
+
201
+ h.decrypt("mxi8XH87").must_equal [25, 100, 950]
202
+ h.decrypt("KQcmkIW8hX").must_equal [5,200,195, 1]
203
+ end
204
+ end
205
+
206
+ describe "decrypt_hex" do
207
+ it "decrypts hex string" do
208
+ hashids.decrypt_hex("lzY").must_equal "FA"
209
+ hashids.decrypt_hex("eBMrb").must_equal "FF1A"
210
+ hashids.decrypt_hex("D9NPE").must_equal "12ABC"
146
211
  end
147
212
  end
148
213
 
149
214
  describe "setup" do
150
- it "raises an exception if the alphabet has less than 4 unique chars" do
215
+ it "raises an exception if the alphabet has less than 16 unique chars" do
151
216
  -> { Hashids.new('salt', 0, 'abc') }.
152
217
  must_raise Hashids::AlphabetError
153
218
  end
@@ -169,4 +234,69 @@ describe Hashids do
169
234
  must_raise Hashids::AlphabetError
170
235
  end
171
236
  end
237
+
238
+ describe "protected methods" do
239
+
240
+ describe "unhash" do
241
+ it "unhashes" do
242
+ hashids.send(:unhash, 'bb', 'abc').must_equal 4
243
+ hashids.send(:unhash, 'aaa', 'abc').must_equal 0
244
+ hashids.send(:unhash, 'cba', 'abc').must_equal 21
245
+ hashids.send(:unhash, 'cbaabc', 'abc').must_equal 572
246
+ hashids.send(:unhash, 'aX11b', 'abcXYZ123').must_equal 2728
247
+ end
248
+ end
249
+
250
+ describe "decode" do
251
+ it "decodes" do
252
+ hashids.send(:decode, 'NV', alphabet).must_equal [1]
253
+ end
254
+ end
255
+
256
+ describe "consistent_shuffle" do
257
+ it "returns the alphabet if empty salt" do
258
+ hashids.send(:consistent_shuffle, default_alphabet, '').
259
+ must_equal default_alphabet
260
+ end
261
+
262
+ it "shuffles consistently" do
263
+ hashids.send(:consistent_shuffle, 'ab', salt).must_equal 'ba'
264
+ hashids.send(:consistent_shuffle, 'abc', salt).must_equal 'bca'
265
+ hashids.send(:consistent_shuffle, 'abcd', salt).must_equal 'cadb'
266
+ hashids.send(:consistent_shuffle, 'abcde', salt).must_equal 'dceba'
267
+
268
+ hashids.send(:consistent_shuffle, default_alphabet, 'salt').
269
+ must_equal "f17a8zvCwo0iuqYDXlJ4RmAS2end5gh" +
270
+ "TcpjbOWLK9GFyE6xUI3ZBMQtPsNHrkV"
271
+
272
+ hashids.send(:consistent_shuffle, 'abcdefghijklmnopqrstuvwxyz', salt).
273
+ must_equal 'fcaodykrgqvblxjwmtupzeisnh'
274
+ end
275
+ end
276
+
277
+ describe "hash" do
278
+ it "hashes" do
279
+ hashids.send(:hash, 12, 'abcdefg').must_equal "bf"
280
+ hashids.send(:hash, 42, 'abcdefg').must_equal "ga"
281
+ hashids.send(:hash, 123, 'abcdefg').must_equal "cde"
282
+ hashids.send(:hash, 1024, 'abcdefg').must_equal "cggc"
283
+ hashids.send(:hash, 950000, 'abcdefg').must_equal "bbadeefc"
284
+ hashids.send(:hash, 950000, 'åäö-ÅÄÖ').must_equal "ääå-ÅÅÄö"
285
+ hashids.send(:hash, 3500000, 'abcdefg').must_equal "ebfbfaea"
286
+ hashids.send(:hash, 3500000, 'Xyz01-å').must_equal "1y-y-X1X"
287
+ end
288
+ end
289
+
290
+ describe "unhash" do
291
+ it "unhashes" do
292
+ hashids.send(:unhash, 'abbd', 'abcdefg').must_equal 59
293
+ hashids.send(:unhash, 'abcd', 'abcdefg').must_equal 66
294
+ hashids.send(:unhash, 'acac', 'abcdefg').must_equal 100
295
+ hashids.send(:unhash, 'acfg', 'abcdefg').must_equal 139
296
+ hashids.send(:unhash, 'x21y', 'xyz1234').must_equal 218
297
+ hashids.send(:unhash, 'yy44', 'xyz1234').must_equal 440
298
+ hashids.send(:unhash, '1xzz', 'xyz1234').must_equal 1045
299
+ end
300
+ end
301
+ end
172
302
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashids
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Hellberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-13 00:00:00.000000000 Z
11
+ date: 2013-09-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Use hashids when you do not want to expose your database ids to the user.
14
14
  email:
@@ -46,11 +46,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
46
  version: '0'
47
47
  requirements: []
48
48
  rubyforge_project:
49
- rubygems_version: 2.0.3
49
+ rubygems_version: 2.0.5
50
50
  signing_key:
51
51
  specification_version: 4
52
52
  summary: Generate YouTube-like hashes from one or many numbers.
53
53
  test_files:
54
54
  - spec/hashids_profile.rb
55
55
  - spec/hashids_spec.rb
56
- has_rdoc: